From 8c02a74dd31b4d4ccb5efed6c4aa243b16bc7979 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 13:22:47 +0200 Subject: [PATCH 01/14] Add helper to find runtime --- .prettierignore | 3 +- server/src/find-runtime.ts | 121 +++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 server/src/find-runtime.ts diff --git a/.prettierignore b/.prettierignore index 459fd3949..b0f5bcefa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,5 @@ server/out analysis/examples analysis/reanalyze/examples -tools/tests \ No newline at end of file +tools/tests +.history/ \ No newline at end of file diff --git a/server/src/find-runtime.ts b/server/src/find-runtime.ts new file mode 100644 index 000000000..ec6e5a18c --- /dev/null +++ b/server/src/find-runtime.ts @@ -0,0 +1,121 @@ +import { readdir, stat as statAsync } from "fs/promises"; +import { join } from "path"; + +// Efficient parallel folder traversal to find node_modules directories +async function findNodeModulesDirs( + rootPath: string, + maxDepth = 12, +): Promise { + const nodeModulesDirs: string[] = []; + const stack: Array<{ dir: string; depth: number }> = [ + { dir: rootPath, depth: 0 }, + ]; + const visited = new Set(); + + while (stack.length) { + const { dir, depth } = stack.pop()!; + if (depth > maxDepth || visited.has(dir)) continue; + visited.add(dir); + + let entries: string[]; + try { + entries = await readdir(dir); + } catch { + continue; + } + + if (entries.includes("node_modules")) { + const nm = join(dir, "node_modules"); + try { + const st = await statAsync(nm); + if (st.isDirectory()) { + nodeModulesDirs.push(nm); + // Do NOT push deeper here to keep same behavior (stop at first node_modules in this branch) + continue; + } + } catch {} + } + + for (const entry of entries) { + if (entry === "node_modules" || entry.startsWith(".")) continue; + const full = join(dir, entry); + try { + const st = await statAsync(full); + if (st.isDirectory()) { + stack.push({ dir: full, depth: depth + 1 }); + } + } catch {} + } + } + + return nodeModulesDirs; +} + +// Custom function to find Deno vendorized @rescript/runtime directories +async function findDenoRescriptRuntime(nodeModulesPath: string) { + // We only care about the Deno vendorized layout: + // /.deno/@rescript+runtime@/node_modules/@rescript/runtime + const denoRoot = join(nodeModulesPath, ".deno"); + let entries: string[]; + try { + entries = await readdir(denoRoot); + } catch { + return []; + } + + // Collect all @rescript+runtime@ vendor dirs + const vendorDirs = entries.filter((e) => e.startsWith("@rescript+runtime@")); + if (vendorDirs.length === 0) return []; + + // Optionally pick β€œlatest” by version; for now we return all valid matches. + const results: string[] = []; + for (const dir of vendorDirs) { + const runtimePath = join( + denoRoot, + dir, + "node_modules", + "@rescript", + "runtime", + ); + try { + const st = await statAsync(runtimePath); + if (st.isDirectory()) results.push(runtimePath); + } catch { + // Ignore inaccessible / missing path + } + } + + return results; +} + +export async function findRuntime(project: string) { + // Find all node_modules directories using efficient traversal + const node_modules = await findNodeModulesDirs(project); + + const rescriptRuntimeDirs = await Promise.all( + node_modules.map(async (nm) => { + const results = []; + + // Check for standard layout: @rescript/runtime + const standardPath = join(nm, "@rescript", "runtime"); + try { + const stat = await statAsync(standardPath); + if (stat.isDirectory()) { + results.push(standardPath); + // If we found standard layout, no need to search for Deno layouts + return results; + } + } catch (e) { + // Directory doesn't exist, continue + } + + // Only check for Deno vendorized layouts if standard layout wasn't found + const denoResults = await findDenoRescriptRuntime(nm); + results.push(...denoResults); + + return results; + }), + ).then((results) => results.flatMap((x) => x)); + + return rescriptRuntimeDirs; +} From b108dfb1ec920a854d26f1373fb736cb14da037c Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 14:29:50 +0200 Subject: [PATCH 02/14] Bump used node version to 20 --- package-lock.json | 21 ++++++++++++++++----- package.json | 2 +- tsconfig.json | 4 ++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 771876b5d..597dda11b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "semver": "^7.7.2" }, "devDependencies": { - "@types/node": "^14.14.41", + "@types/node": "^20.19.13", "@types/semver": "^7.7.0", "@types/vscode": "1.68.0", "esbuild": "^0.20.1", @@ -41,10 +41,14 @@ } }, "node_modules/@types/node": { - "version": "14.14.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz", - "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==", - "dev": true + "version": "20.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", + "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, "node_modules/@types/semver": { "version": "7.7.0", @@ -138,6 +142,13 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 82e929196..67f82f27b 100644 --- a/package.json +++ b/package.json @@ -259,7 +259,7 @@ "bundle": "npm run bundle-server && npm run bundle-client" }, "devDependencies": { - "@types/node": "^14.14.41", + "@types/node": "^20.19.13", "@types/semver": "^7.7.0", "@types/vscode": "1.68.0", "esbuild": "^0.20.1", diff --git a/tsconfig.json b/tsconfig.json index cae98cfda..0f5dff06a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2019", - "lib": ["ES2019"], + "target": "ES2020", + "lib": ["ES2020"], "outDir": "out", "rootDir": "src", "sourceMap": true From 718e98620b055672800845f373eb90d78f9e3909 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 14:31:17 +0200 Subject: [PATCH 03/14] Extract bsc arg collection for different systems. Apply RESCRIPT_RUNTIME to rewatch --- server/src/bsc-args/bsb.ts | 59 ++++++++ server/src/bsc-args/rewatch.ts | 133 +++++++++++++++++ server/src/incrementalCompilation.ts | 214 ++------------------------- server/src/projectFiles.ts | 2 +- 4 files changed, 204 insertions(+), 204 deletions(-) create mode 100644 server/src/bsc-args/bsb.ts create mode 100644 server/src/bsc-args/rewatch.ts diff --git a/server/src/bsc-args/bsb.ts b/server/src/bsc-args/bsb.ts new file mode 100644 index 000000000..8566db7b1 --- /dev/null +++ b/server/src/bsc-args/bsb.ts @@ -0,0 +1,59 @@ +import * as path from "path"; +import fs from "fs"; +import { IncrementallyCompiledFileInfo } from "../incrementalCompilation"; +import { buildNinjaPartialPath } from "../constants"; + +export type BsbCompilerArgs = string[]; + +export async function getBsbBscArgs( + entry: IncrementallyCompiledFileInfo, +): Promise { + const buildNinjaPath = path.resolve( + entry.project.rootPath, + buildNinjaPartialPath, + ); + + let stat: fs.Stats; + try { + stat = await fs.promises.stat(buildNinjaPath); + } catch { + return null; + } + + const cache = entry.buildNinja; + if (cache && cache.fileMtime >= stat.mtimeMs) { + return cache.rawExtracted; + } + + const fh = await fs.promises.open(buildNinjaPath, "r"); + try { + let captureNext = false; + let haveAst = false; + const captured: string[] = []; + + for await (const rawLine of fh.readLines()) { + const line = String(rawLine).trim(); + if (captureNext) { + captured.push(line); + captureNext = false; + if (haveAst && captured.length === 2) break; // got ast + mij + } + if (line.startsWith("rule astj")) { + captureNext = true; + haveAst = true; + } else if (line.startsWith("rule mij")) { + captureNext = true; + } + } + + if (captured.length !== 2) return null; + + entry.buildNinja = { + fileMtime: stat.mtimeMs, + rawExtracted: captured, + }; + return captured; + } finally { + await fh.close(); + } +} diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts new file mode 100644 index 000000000..2e5372a99 --- /dev/null +++ b/server/src/bsc-args/rewatch.ts @@ -0,0 +1,133 @@ +import * as path from "path"; +import * as utils from "../utils"; +import * as cp from "node:child_process"; +import semver from "semver"; +import { + debug, + IncrementallyCompiledFileInfo, +} from "../incrementalCompilation"; +import type { projectFiles } from "../projectFiles"; +import { findRuntime } from "../find-runtime"; + +export type RewatchCompilerArgs = { + compiler_args: Array; + parser_args: Array; +}; + +export async function getRewatchBscArgs( + projectsFiles: Map, + entry: IncrementallyCompiledFileInfo, +): Promise { + const rewatchCacheEntry = entry.buildRewatch; + + if ( + rewatchCacheEntry != null && + rewatchCacheEntry.lastFile === entry.file.sourceFilePath + ) { + return Promise.resolve(rewatchCacheEntry.compilerArgs); + } + + try { + const project = projectsFiles.get(entry.project.rootPath); + if (project?.rescriptVersion == null) return null; + let rewatchPath = path.resolve( + entry.project.workspaceRootPath, + "node_modules/@rolandpeelen/rewatch/rewatch", + ); + let rescriptRewatchPath = null; + if ( + semver.valid(project.rescriptVersion) && + semver.satisfies(project.rescriptVersion as string, ">11", { + includePrerelease: true, + }) + ) { + rescriptRewatchPath = await utils.findRewatchBinary( + entry.project.workspaceRootPath, + ); + } + + if ( + semver.valid(project.rescriptVersion) && + semver.satisfies(project.rescriptVersion as string, ">=12.0.0-beta.1", { + includePrerelease: true, + }) + ) { + rescriptRewatchPath = await utils.findRescriptExeBinary( + entry.project.workspaceRootPath, + ); + } + + if (rescriptRewatchPath != null) { + rewatchPath = rescriptRewatchPath; + if (debug()) { + console.log( + `Found rewatch binary bundled with v12: ${rescriptRewatchPath}`, + ); + } + } else { + if (debug()) { + console.log("Did not find rewatch binary bundled with v12"); + } + } + + const rewatchArguments = semver.satisfies( + project.rescriptVersion, + ">=12.0.0-beta.2", + { includePrerelease: true }, + ) + ? ["compiler-args", entry.file.sourceFilePath] + : [ + "--rescript-version", + project.rescriptVersion, + "--compiler-args", + entry.file.sourceFilePath, + ]; + const bscExe = await utils.findBscExeBinary( + entry.project.workspaceRootPath, + ); + const env = {}; + if (bscExe != null) { + (env as any)["RESCRIPT_BSC_EXE"] = bscExe; + } + + // TODO: We should check a potential configured value + // Users should be able to provide this themselves if they like. + const rescriptRuntimes = await findRuntime(entry.project.workspaceRootPath); + + if (debug()) { + if (rescriptRuntimes.length === 0) { + console.log( + `Did not find @rescript/runtime directory for ${entry.project.workspaceRootPath}`, + ); + } else if (rescriptRuntimes.length > 1) { + console.warn( + `Found multiple @rescript/runtime directories, using the first one as RESCRIPT_RUNTIME: ${rescriptRuntimes.join(", ")}`, + ); + } else { + console.log( + `Found @rescript/runtime directory: ${rescriptRuntimes.join(", ")}`, + ); + } + } + + if (rescriptRuntimes.length > 0) { + (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntimes[0]; + } else { + // TODO: if no runtime was found, we should let the user know + } + + const compilerArgs = JSON.parse( + cp.execFileSync(rewatchPath, rewatchArguments, { env }).toString().trim(), + ) as RewatchCompilerArgs; + + entry.buildRewatch = { + lastFile: entry.file.sourceFilePath, + compilerArgs: compilerArgs, + }; + + return compilerArgs; + } catch (e) { + console.error(e); + return null; + } +} diff --git a/server/src/incrementalCompilation.ts b/server/src/incrementalCompilation.ts index 1f1a13ed1..6c1952d91 100644 --- a/server/src/incrementalCompilation.ts +++ b/server/src/incrementalCompilation.ts @@ -12,8 +12,10 @@ import config, { send } from "./config"; import * as c from "./constants"; import { fileCodeActions } from "./codeActions"; import { projectsFiles } from "./projectFiles"; +import { getRewatchBscArgs, RewatchCompilerArgs } from "./bsc-args/rewatch"; +import { BsbCompilerArgs, getBsbBscArgs } from "./bsc-args/bsb"; -function debug() { +export function debug() { return ( config.extensionConfiguration.incrementalTypechecking?.debugLogging ?? false ); @@ -25,12 +27,7 @@ const INCREMENTAL_FILE_FOLDER_LOCATION = path.join( INCREMENTAL_FOLDER_NAME, ); -type RewatchCompilerArgs = { - compiler_args: Array; - parser_args: Array; -}; - -type IncrementallyCompiledFileInfo = { +export type IncrementallyCompiledFileInfo = { file: { /** File type. */ extension: ".res" | ".resi"; @@ -53,7 +50,7 @@ type IncrementallyCompiledFileInfo = { /** When build.ninja was last modified. Used as a cache key. */ fileMtime: number; /** The raw, extracted needed info from build.ninja. Needs processing. */ - rawExtracted: Array; + rawExtracted: BsbCompilerArgs; } | null; /** Cache for rewatch compiler args. */ buildRewatch: { @@ -181,202 +178,13 @@ export function cleanUpIncrementalFiles( ); }); } -function getBscArgs( - entry: IncrementallyCompiledFileInfo, -): Promise | RewatchCompilerArgs | null> { - const buildNinjaPath = path.resolve( - entry.project.rootPath, - c.buildNinjaPartialPath, - ); - const rewatchLockfile = path.resolve( - entry.project.workspaceRootPath, - c.rewatchLockPartialPath, - ); - const rescriptLockfile = path.resolve( - entry.project.workspaceRootPath, - c.rescriptLockPartialPath, - ); - let buildSystem: "bsb" | "rewatch" | null = null; - - let stat: fs.Stats | null = null; - try { - stat = fs.statSync(buildNinjaPath); - buildSystem = "bsb"; - } catch {} - try { - stat = fs.statSync(rewatchLockfile); - buildSystem = "rewatch"; - } catch {} - try { - stat = fs.statSync(rescriptLockfile); - buildSystem = "rewatch"; - } catch {} - if (buildSystem == null) { - console.log("Did not find build.ninja or rewatch.lock, cannot proceed.."); - return Promise.resolve(null); - } else if (debug()) { - console.log( - `Using build system: ${buildSystem} for ${entry.file.sourceFilePath}`, - ); - } - const bsbCacheEntry = entry.buildNinja; - const rewatchCacheEntry = entry.buildRewatch; - - if ( - buildSystem === "bsb" && - bsbCacheEntry != null && - stat != null && - bsbCacheEntry.fileMtime >= stat.mtimeMs - ) { - return Promise.resolve(bsbCacheEntry.rawExtracted); - } - if ( - buildSystem === "rewatch" && - rewatchCacheEntry != null && - rewatchCacheEntry.lastFile === entry.file.sourceFilePath - ) { - return Promise.resolve(rewatchCacheEntry.compilerArgs); - } - return new Promise(async (resolve, _reject) => { - function resolveResult(result: Array | RewatchCompilerArgs) { - if (stat != null && Array.isArray(result)) { - entry.buildSystem = "bsb"; - entry.buildNinja = { - fileMtime: stat.mtimeMs, - rawExtracted: result, - }; - } else if (!Array.isArray(result)) { - entry.buildSystem = "rewatch"; - entry.buildRewatch = { - lastFile: entry.file.sourceFilePath, - compilerArgs: result, - }; - } - resolve(result); - } - if (buildSystem === "bsb") { - const fileStream = fs.createReadStream(buildNinjaPath, { - encoding: "utf8", - }); - fileStream.on("error", (err) => { - console.error("File stream error:", err); - resolveResult([]); - }); - const rl = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity, - }); - let captureNextLine = false; - let done = false; - let stopped = false; - const captured: Array = []; - rl.on("line", (line) => { - line = line.trim(); // Normalize line endings - if (stopped) { - return; - } - if (captureNextLine) { - captured.push(line); - captureNextLine = false; - } - if (done) { - // Not sure if fileStream.destroy is necessary, rl.close() will handle it gracefully. - // fileStream.destroy(); - rl.close(); - resolveResult(captured); - stopped = true; - return; - } - if (line.startsWith("rule astj")) { - captureNextLine = true; - } - if (line.startsWith("rule mij")) { - captureNextLine = true; - done = true; - } - }); - rl.on("error", (err) => { - console.error("Readline error:", err); - resolveResult([]); - }); - rl.on("close", () => { - resolveResult(captured); - }); - } else if (buildSystem === "rewatch") { - try { - const project = projectsFiles.get(entry.project.rootPath); - if (project?.rescriptVersion == null) return; - let rewatchPath = path.resolve( - entry.project.workspaceRootPath, - "node_modules/@rolandpeelen/rewatch/rewatch", - ); - let rescriptRewatchPath = null; - if ( - semver.valid(project.rescriptVersion) && - semver.satisfies(project.rescriptVersion as string, ">11", { - includePrerelease: true, - }) - ) { - rescriptRewatchPath = await utils.findRewatchBinary( - entry.project.workspaceRootPath, - ); - } - - if ( - semver.valid(project.rescriptVersion) && - semver.satisfies( - project.rescriptVersion as string, - ">=12.0.0-beta.1", - { includePrerelease: true }, - ) - ) { - rescriptRewatchPath = await utils.findRescriptExeBinary( - entry.project.workspaceRootPath, - ); - } - - if (rescriptRewatchPath != null) { - rewatchPath = rescriptRewatchPath; - if (debug()) { - console.log( - `Found rewatch binary bundled with v12: ${rescriptRewatchPath}`, - ); - } - } else { - if (debug()) { - console.log("Did not find rewatch binary bundled with v12"); - } - } - - const rewatchArguments = semver.satisfies( - project.rescriptVersion, - ">=12.0.0-beta.2", - { includePrerelease: true }, - ) - ? ["compiler-args", entry.file.sourceFilePath] - : [ - "--rescript-version", - project.rescriptVersion, - "--compiler-args", - entry.file.sourceFilePath, - ]; - const bscExe = await utils.findBscExeBinary( - entry.project.workspaceRootPath, - ); - const env = bscExe != null ? { RESCRIPT_BSC_EXE: bscExe } : undefined; - const compilerArgs = JSON.parse( - cp - .execFileSync(rewatchPath, rewatchArguments, { env }) - .toString() - .trim(), - ) as RewatchCompilerArgs; - resolveResult(compilerArgs); - } catch (e) { - console.error(e); - } - } - }); +export async function getBscArgs( + entry: IncrementallyCompiledFileInfo, +): Promise { + return entry.buildSystem === "bsb" + ? await getBsbBscArgs(entry) + : await getRewatchBscArgs(projectsFiles, entry); } function argCouples(argList: string[]): string[][] { diff --git a/server/src/projectFiles.ts b/server/src/projectFiles.ts index 4a1787a0c..82582e123 100644 --- a/server/src/projectFiles.ts +++ b/server/src/projectFiles.ts @@ -5,7 +5,7 @@ export type filesDiagnostics = { [key: string]: p.Diagnostic[]; }; -interface projectFiles { +export interface projectFiles { openFiles: Set; filesWithDiagnostics: Set; filesDiagnostics: filesDiagnostics; From 959c336459fc27a81fe544c3ba2b37162fbc4ea2 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 14:37:13 +0200 Subject: [PATCH 04/14] Add bsb remark --- server/src/bsc-args/bsb.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/bsc-args/bsb.ts b/server/src/bsc-args/bsb.ts index 8566db7b1..19263e1fd 100644 --- a/server/src/bsc-args/bsb.ts +++ b/server/src/bsc-args/bsb.ts @@ -5,6 +5,9 @@ import { buildNinjaPartialPath } from "../constants"; export type BsbCompilerArgs = string[]; +// TODO: I guess somewhere in here, when the version is v12 beta 10 or later, +// We need to pass -rescript-runtime as argument as well. + export async function getBsbBscArgs( entry: IncrementallyCompiledFileInfo, ): Promise { From 9943ab39e91172f4232c5b07d5227f3d54031887 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 14:46:21 +0200 Subject: [PATCH 05/14] Check both lock files --- server/src/incrementalCompilation.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/incrementalCompilation.ts b/server/src/incrementalCompilation.ts index 6c1952d91..921e4e4e0 100644 --- a/server/src/incrementalCompilation.ts +++ b/server/src/incrementalCompilation.ts @@ -255,14 +255,18 @@ function triggerIncrementalCompilationOfFile( return; } - const projectRewatchLockfile = path.resolve( - projectRootPath, - c.rewatchLockPartialPath, - ); + const projectRewatchLockfiles = [ + path.resolve(projectRootPath, c.rewatchLockPartialPath), + path.resolve(projectRootPath, c.rescriptLockPartialPath), + ]; let foundRewatchLockfileInProjectRoot = false; - if (fs.existsSync(projectRewatchLockfile)) { + if (projectRewatchLockfiles.some((lockFile) => fs.existsSync(lockFile))) { foundRewatchLockfileInProjectRoot = true; + } else if (debug()) { + console.log( + `Did not find ${projectRewatchLockfiles.join(" or ")} in project root, assuming bsb`, + ); } // if we find a rewatch.lock in the project root, it's a compilation of a local package From 821ce108613a25dab11f6446cac4d4bd3a3de13a Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 14:48:27 +0200 Subject: [PATCH 06/14] Add caching todo --- server/src/bsc-args/rewatch.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index 2e5372a99..ad4318582 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -92,6 +92,8 @@ export async function getRewatchBscArgs( // TODO: We should check a potential configured value // Users should be able to provide this themselves if they like. + + // TODO: We should also cache this value if we found it. const rescriptRuntimes = await findRuntime(entry.project.workspaceRootPath); if (debug()) { From c50424db1b67b1c27521ee37a6ed983eba659992 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 14:50:35 +0200 Subject: [PATCH 07/14] Add script to test finding runtime --- scripts/find-runtime.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 scripts/find-runtime.ts diff --git a/scripts/find-runtime.ts b/scripts/find-runtime.ts new file mode 100644 index 000000000..e0b2a8987 --- /dev/null +++ b/scripts/find-runtime.ts @@ -0,0 +1,32 @@ +// benchmark +const start = process.hrtime.bigint(); + +// start code +const args = process.argv.slice(2); + +if (args.length === 0) { + console.log(` +Usage: node find-runtime.mjs +Find @rescript/runtime directories in a project's node_modules. +Arguments: + project-folder Path to the project directory to search +Examples: + node find-runtime.mjs /path/to/project + node find-runtime.mjs . +`); + process.exit(1); +} + +const project = args[args.length - 1]; + +import { findRuntime } from "../server/src/find-runtime.ts"; + +const runtimes = await findRuntime(project); + +console.log("Found @rescript/runtime directories:", runtimes); + +// end code +const end = process.hrtime.bigint(); +const durationMs = Number(end - start) / 1e6; // convert ns β†’ ms + +console.log(`Script took ${durationMs.toFixed(3)}ms`); From b0ec8de3cd956ab9f6d938f2790b750c3a41e123 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 11 Sep 2025 16:54:37 +0200 Subject: [PATCH 08/14] Cache runtime results --- scripts/find-runtime.ts | 4 ++-- server/src/bsc-args/rewatch.ts | 7 ++++--- server/src/find-runtime.ts | 25 ++++++++++++++++++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/scripts/find-runtime.ts b/scripts/find-runtime.ts index e0b2a8987..5f6c13235 100644 --- a/scripts/find-runtime.ts +++ b/scripts/find-runtime.ts @@ -19,9 +19,9 @@ Examples: const project = args[args.length - 1]; -import { findRuntime } from "../server/src/find-runtime.ts"; +import { findRescriptRuntimesInProject } from "../server/src/find-runtime.ts"; -const runtimes = await findRuntime(project); +const runtimes = await findRescriptRuntimesInProject(project); console.log("Found @rescript/runtime directories:", runtimes); diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index ad4318582..8d63761ea 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -7,7 +7,7 @@ import { IncrementallyCompiledFileInfo, } from "../incrementalCompilation"; import type { projectFiles } from "../projectFiles"; -import { findRuntime } from "../find-runtime"; +import { findRescriptRuntimesInProject } from "../find-runtime"; export type RewatchCompilerArgs = { compiler_args: Array; @@ -93,8 +93,9 @@ export async function getRewatchBscArgs( // TODO: We should check a potential configured value // Users should be able to provide this themselves if they like. - // TODO: We should also cache this value if we found it. - const rescriptRuntimes = await findRuntime(entry.project.workspaceRootPath); + const rescriptRuntimes = await findRescriptRuntimesInProject( + entry.project.workspaceRootPath, + ); if (debug()) { if (rescriptRuntimes.length === 0) { diff --git a/server/src/find-runtime.ts b/server/src/find-runtime.ts index ec6e5a18c..1611ec792 100644 --- a/server/src/find-runtime.ts +++ b/server/src/find-runtime.ts @@ -1,5 +1,5 @@ import { readdir, stat as statAsync } from "fs/promises"; -import { join } from "path"; +import { join, resolve } from "path"; // Efficient parallel folder traversal to find node_modules directories async function findNodeModulesDirs( @@ -88,7 +88,7 @@ async function findDenoRescriptRuntime(nodeModulesPath: string) { return results; } -export async function findRuntime(project: string) { +async function findRuntimePath(project: string) { // Find all node_modules directories using efficient traversal const node_modules = await findNodeModulesDirs(project); @@ -117,5 +117,24 @@ export async function findRuntime(project: string) { }), ).then((results) => results.flatMap((x) => x)); - return rescriptRuntimeDirs; + return rescriptRuntimeDirs.map((runtime) => resolve(runtime)); } + +function findRuntimeCached() { + const cache = new Map(); + return async (project: string) => { + if (cache.has(project)) { + return cache.get(project)!; + } + const runtimes = await findRuntimePath(project); + cache.set(project, runtimes); + return runtimes; + }; +} + +/** + * Find all installed @rescript/runtime directories in the given project path. + * In a perfect world, there should be exactly one. + * This function is cached per project path. + */ +export const findRescriptRuntimesInProject = findRuntimeCached(); From ff032c8e3032a85c9b06f82a16cc7fb0596ffdc9 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 12 Sep 2025 08:08:58 +0200 Subject: [PATCH 09/14] bsb already has the runtime argument in ninja file --- server/src/bsc-args/bsb.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/bsc-args/bsb.ts b/server/src/bsc-args/bsb.ts index 19263e1fd..8566db7b1 100644 --- a/server/src/bsc-args/bsb.ts +++ b/server/src/bsc-args/bsb.ts @@ -5,9 +5,6 @@ import { buildNinjaPartialPath } from "../constants"; export type BsbCompilerArgs = string[]; -// TODO: I guess somewhere in here, when the version is v12 beta 10 or later, -// We need to pass -rescript-runtime as argument as well. - export async function getBsbBscArgs( entry: IncrementallyCompiledFileInfo, ): Promise { From c843ad2d1f7d81a96db310f341047bece3784134 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 12 Sep 2025 08:33:15 +0200 Subject: [PATCH 10/14] Use rescript runtime from configuration if provided --- README.md | 6 ++-- package.json | 11 +++++- server/src/bsc-args/rewatch.ts | 66 +++++++++++++++++++++------------- server/src/config.ts | 1 + 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 3071788f9..1227ab8ae 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,20 @@ ## Contents +- [Contents](#contents) - [πŸ“ Prerequisite](#-prerequisite) - [🌈 Supported Themes](#-supported-themes) - [πŸ’‘ Features](#-features) - [πŸ“₯ Installation](#-installation) + - [Pre-release channel](#pre-release-channel) - [πŸ“¦ Commands](#-commands) - [πŸ”¨ Settings](#-settings) - [πŸš€ Code Analyzer](#-code-analyzer) - [Configuring the Code Analyzer](#configuring-the-code-analyzer) - [Usage](#usage) - [Caveats](#caveats) -- [πŸͺ„ Tips & Tricks](#-tips--tricks) +- [πŸͺ„ Tips \& Tricks](#-tips--tricks) - [Hide generated files](#hide-generated-files) -- [⌨️ Use with Other Editors](#️-use-with-other-editors) - [πŸ“° Changelog](#-changelog) - [πŸ‘ How to Contribute](#-how-to-contribute) - [πŸ“„ License](#-license) @@ -102,6 +103,7 @@ You'll find all ReScript specific settings under the scope `rescript.settings`. | Prompt to Start Build | If there's no ReScript build running already in the opened project, the extension will prompt you and ask if you want to start a build automatically. You can turn off this automatic prompt via the setting `rescript.settings.askToStartBuild`. | | ReScript Binary Path | The extension will look for the existence of a `node_modules/.bin/rescript` file and use its directory as the `binaryPath`. If it does not find it at the project root (which is where the nearest `rescript.json` resides), it goes up folders in the filesystem recursively until it either finds it (often the case in monorepos) or hits the top level. To override this lookup process, the path can be configured explicitly using the setting `rescript.settings.binaryPath` | | ReScript Platform Path | The extension will look for the existence of a `node_modules/rescript` directory and use the subdirectory corresponding to the current platform as the `platformPath`. If it does not find it at the project root (which is where the nearest `rescript.json` resides), it goes up folders in the filesystem recursively until it either finds it (often the case in monorepos) or hits the top level. To override this lookup process, the path can be configured explicitly using the setting `rescript.settings.platformPath` | +| ReScript Runtime Path | The extension will look for the existence of a `node_modules/@rescript/runtime` directory (ReScript v12 beta 11+). To override this lookup process, the path can be configured explicitly using the setting `rescript.settings.runtimePath`. | | Inlay Hints (experimental) | This allows an editor to place annotations inline with text to display type hints. Enable using `rescript.settings.inlayHints.enable: true` | | Code Lens (experimental) | This tells the editor to add code lenses to function definitions, showing its full type above the definition. Enable using `rescript.settings.codeLens: true` | | Signature Help | This tells the editor to show signature help when you're writing function calls. Enable using `rescript.settings.signatureHelp.enabled: true` | diff --git a/package.json b/package.json index 67f82f27b..530cc76ea 100644 --- a/package.json +++ b/package.json @@ -207,6 +207,14 @@ "default": null, "description": "Path to the directory where platform-specific ReScript binaries are. You can use it if you haven't or don't want to use the installed ReScript from node_modules in your project." }, + "rescript.settings.runtimePath": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Optional path to the directory containing the @rescript/runtime package. Set this if your tooling is unable to automatically locate the package in your project." + }, "rescript.settings.compileStatus.enable": { "type": "boolean", "default": true, @@ -268,5 +276,6 @@ }, "dependencies": { "semver": "^7.7.2" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index 8d63761ea..7d239088c 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -7,6 +7,7 @@ import { IncrementallyCompiledFileInfo, } from "../incrementalCompilation"; import type { projectFiles } from "../projectFiles"; +import config from "../config"; import { findRescriptRuntimesInProject } from "../find-runtime"; export type RewatchCompilerArgs = { @@ -14,6 +15,44 @@ export type RewatchCompilerArgs = { parser_args: Array; }; +async function getRuntimePath( + entry: IncrementallyCompiledFileInfo, +): Promise { + let rescriptRuntime: string | null = + config.extensionConfiguration.runtimePath ?? null; + + if (rescriptRuntime !== null) { + if (debug()) { + console.log( + `Using configured runtime path as RESCRIPT_RUNTIME: ${rescriptRuntime}`, + ); + } + return rescriptRuntime; + } + + const rescriptRuntimes = await findRescriptRuntimesInProject( + entry.project.workspaceRootPath, + ); + + if (debug()) { + if (rescriptRuntimes.length === 0) { + console.log( + `Did not find @rescript/runtime directory for ${entry.project.workspaceRootPath}`, + ); + } else if (rescriptRuntimes.length > 1) { + console.warn( + `Found multiple @rescript/runtime directories, using the first one as RESCRIPT_RUNTIME: ${rescriptRuntimes.join(", ")}`, + ); + } else { + console.log( + `Found @rescript/runtime directory: ${rescriptRuntimes.join(", ")}`, + ); + } + } + + return rescriptRuntimes.at(0) ?? null; +} + export async function getRewatchBscArgs( projectsFiles: Map, entry: IncrementallyCompiledFileInfo, @@ -90,31 +129,10 @@ export async function getRewatchBscArgs( (env as any)["RESCRIPT_BSC_EXE"] = bscExe; } - // TODO: We should check a potential configured value - // Users should be able to provide this themselves if they like. - - const rescriptRuntimes = await findRescriptRuntimesInProject( - entry.project.workspaceRootPath, - ); - - if (debug()) { - if (rescriptRuntimes.length === 0) { - console.log( - `Did not find @rescript/runtime directory for ${entry.project.workspaceRootPath}`, - ); - } else if (rescriptRuntimes.length > 1) { - console.warn( - `Found multiple @rescript/runtime directories, using the first one as RESCRIPT_RUNTIME: ${rescriptRuntimes.join(", ")}`, - ); - } else { - console.log( - `Found @rescript/runtime directory: ${rescriptRuntimes.join(", ")}`, - ); - } - } + let rescriptRuntime: string | null = await getRuntimePath(entry); - if (rescriptRuntimes.length > 0) { - (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntimes[0]; + if (rescriptRuntime !== null) { + (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntime; } else { // TODO: if no runtime was found, we should let the user know } diff --git a/server/src/config.ts b/server/src/config.ts index e8055e379..97b89985a 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -11,6 +11,7 @@ export interface extensionConfiguration { codeLens?: boolean; binaryPath?: string | null; platformPath?: string | null; + runtimePath?: string | null; signatureHelp?: { enabled?: boolean; forConstructorPayloads?: boolean; From 39b7ed2d3f2e3a45c90787600e52cd9456b6973a Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 12 Sep 2025 08:35:34 +0200 Subject: [PATCH 11/14] Include version check before adding runtime --- server/src/bsc-args/rewatch.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index 7d239088c..df1de0a17 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -131,7 +131,12 @@ export async function getRewatchBscArgs( let rescriptRuntime: string | null = await getRuntimePath(entry); - if (rescriptRuntime !== null) { + if ( + rescriptRuntime !== null && + semver.satisfies(project.rescriptVersion, ">=12.0.0-beta.11", { + includePrerelease: true, + }) + ) { (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntime; } else { // TODO: if no runtime was found, we should let the user know From 3d1700fae1829d1bfe2e55f551036a05bc3ac03a Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 12 Sep 2025 08:53:53 +0200 Subject: [PATCH 12/14] Send message to lsp client when runtime was not found. --- server/src/bsc-args/rewatch.ts | 31 ++++++++++++++++++++++------ server/src/incrementalCompilation.ts | 13 ++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index df1de0a17..1bc84ef78 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -1,6 +1,7 @@ import * as path from "path"; import * as utils from "../utils"; import * as cp from "node:child_process"; +import * as p from "vscode-languageserver-protocol"; import semver from "semver"; import { debug, @@ -9,6 +10,7 @@ import { import type { projectFiles } from "../projectFiles"; import config from "../config"; import { findRescriptRuntimesInProject } from "../find-runtime"; +import { jsonrpcVersion } from "../constants"; export type RewatchCompilerArgs = { compiler_args: Array; @@ -54,6 +56,7 @@ async function getRuntimePath( } export async function getRewatchBscArgs( + send: (msg: p.Message) => void, projectsFiles: Map, entry: IncrementallyCompiledFileInfo, ): Promise { @@ -129,17 +132,33 @@ export async function getRewatchBscArgs( (env as any)["RESCRIPT_BSC_EXE"] = bscExe; } - let rescriptRuntime: string | null = await getRuntimePath(entry); - + // For ReScript >= 12.0.0-beta.11 we need to set RESCRIPT_RUNTIME if ( - rescriptRuntime !== null && semver.satisfies(project.rescriptVersion, ">=12.0.0-beta.11", { includePrerelease: true, }) ) { - (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntime; - } else { - // TODO: if no runtime was found, we should let the user know + let rescriptRuntime: string | null = await getRuntimePath(entry); + + if (rescriptRuntime !== null) { + (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntime; + } else { + // If no runtime was found, we should let the user know. + let params: p.ShowMessageParams = { + type: p.MessageType.Error, + message: + `[Incremental type checking] The @rescript/runtime package was not found in your project. ` + + `It is normally included with ReScript, but either it's missing or could not be detected. ` + + `Check that it exists in your dependencies, or configure 'rescript.settings.runtimePath' to point to it. ` + + `Without this package, incremental type checking may not work as expected.`, + }; + let message: p.NotificationMessage = { + jsonrpc: jsonrpcVersion, + method: "window/showMessage", + params: params, + }; + send(message); + } } const compilerArgs = JSON.parse( diff --git a/server/src/incrementalCompilation.ts b/server/src/incrementalCompilation.ts index 921e4e4e0..849047e4c 100644 --- a/server/src/incrementalCompilation.ts +++ b/server/src/incrementalCompilation.ts @@ -180,11 +180,12 @@ export function cleanUpIncrementalFiles( } export async function getBscArgs( + send: (msg: p.Message) => void, entry: IncrementallyCompiledFileInfo, ): Promise { return entry.buildSystem === "bsb" ? await getBsbBscArgs(entry) - : await getRewatchBscArgs(projectsFiles, entry); + : await getRewatchBscArgs(send, projectsFiles, entry); } function argCouples(argList: string[]): string[][] { @@ -331,6 +332,7 @@ function triggerIncrementalCompilationOfFile( }; incrementalFileCacheEntry.project.callArgs = figureOutBscArgs( + send, incrementalFileCacheEntry, ); originalTypeFileToFilePath.set( @@ -371,7 +373,10 @@ function verifyTriggerToken(filePath: string, triggerToken: number): boolean { const isWindows = os.platform() === "win32"; -async function figureOutBscArgs(entry: IncrementallyCompiledFileInfo) { +async function figureOutBscArgs( + send: (msg: p.Message) => void, + entry: IncrementallyCompiledFileInfo, +) { const project = projectsFiles.get(entry.project.rootPath); if (project?.rescriptVersion == null) { if (debug()) { @@ -382,7 +387,7 @@ async function figureOutBscArgs(entry: IncrementallyCompiledFileInfo) { } return null; } - const res = await getBscArgs(entry); + const res = await getBscArgs(send, entry); if (res == null) return null; let astArgs: Array> = []; let buildArgs: Array> = []; @@ -483,7 +488,7 @@ async function compileContents( const triggerToken = entry.compilation?.triggerToken; let callArgs = await entry.project.callArgs; if (callArgs == null) { - const callArgsRetried = await figureOutBscArgs(entry); + const callArgsRetried = await figureOutBscArgs(send, entry); if (callArgsRetried != null) { callArgs = callArgsRetried; entry.project.callArgs = Promise.resolve(callArgsRetried); From 44a91c8956b68f132abfeefb889603b380641be2 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 12 Sep 2025 13:08:40 +0200 Subject: [PATCH 13/14] Remove as any --- server/src/bsc-args/rewatch.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index 1bc84ef78..5e39c4ed6 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -127,9 +127,9 @@ export async function getRewatchBscArgs( const bscExe = await utils.findBscExeBinary( entry.project.workspaceRootPath, ); - const env = {}; + const env: NodeJS.ProcessEnv = {}; if (bscExe != null) { - (env as any)["RESCRIPT_BSC_EXE"] = bscExe; + env["RESCRIPT_BSC_EXE"] = bscExe; } // For ReScript >= 12.0.0-beta.11 we need to set RESCRIPT_RUNTIME @@ -141,7 +141,7 @@ export async function getRewatchBscArgs( let rescriptRuntime: string | null = await getRuntimePath(entry); if (rescriptRuntime !== null) { - (env as any)["RESCRIPT_RUNTIME"] = rescriptRuntime; + env["RESCRIPT_RUNTIME"] = rescriptRuntime; } else { // If no runtime was found, we should let the user know. let params: p.ShowMessageParams = { From 82b656a7fe63d1f084a6c43532ea57fe8f5beb43 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 12 Sep 2025 13:10:04 +0200 Subject: [PATCH 14/14] Add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82622929..5edf1b478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ - Add status bar item tracking compilation state. https://github.com/rescript-lang/rescript-vscode/pull/1119 +#### :house: Internal + +- Find `@rescript/runtime` for Rewatch compiler-args call. https://github.com/rescript-lang/rescript-vscode/pull/1125 + ## 1.64.0 #### :rocket: New Feature