From 32be761b5b940cc922129272565193f7148aaae8 Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Fri, 2 Oct 2026 06:57:53 +0200 Subject: [PATCH 1/5] Resolve RescriptTools.binaryPath through a rescript package export RescriptTools.binaryPath was `@module("#cli/bins")`. An external is emitted at its use site, so the `import ... from "#cli/bins"` lands in the user's compiled module, and Node resolves a `#` specifier against the "imports" field of the package.json closest to that module. Only the rescript package defines `#cli/*`, so in a user project the import fails with ERR_PACKAGE_IMPORT_NOT_DEFINED. A scratch project linking the workspace's rescript, @rescript/runtime and @rescript/darwin-arm64 packages reproduced this. The rescript package now exports "./cli/bins" (cli/common/bins.js) and binaryPath imports "rescript/cli/bins", which resolves from any project that depends on rescript. tests/build_tests/rescript_tools_binary_path compiles a module that prints RescriptTools.binaryPath into a src/ with its own package.json, links node_modules/rescript to the repository and runs it. It fails with ERR_PACKAGE_IMPORT_NOT_DEFINED before this change and prints the rescript-tools path after it; node scripts/test.js -build passes. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristiano Calcagno --- package.json | 1 + packages/@rescript/runtime/RescriptTools.res | 2 +- .../rescript_tools_binary_path/.gitignore | 1 + .../rescript_tools_binary_path/input.js | 27 +++++++++++++++++++ .../rescript_tools_binary_path/rescript.json | 8 ++++++ .../rescript_tools_binary_path/src/Main.res | 1 + .../src/package.json | 5 ++++ 7 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/build_tests/rescript_tools_binary_path/.gitignore create mode 100644 tests/build_tests/rescript_tools_binary_path/input.js create mode 100644 tests/build_tests/rescript_tools_binary_path/rescript.json create mode 100644 tests/build_tests/rescript_tools_binary_path/src/Main.res create mode 100644 tests/build_tests/rescript_tools_binary_path/src/package.json diff --git a/package.json b/package.json index a02236f658c..0114500a8ad 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "exports": { "./lib/es6/*": "./lib/es6/*", "./lib/js/*": "./lib/js/*", + "./cli/bins": "./cli/common/bins.js", "./package.json": "./package.json" }, "imports": { diff --git a/packages/@rescript/runtime/RescriptTools.res b/packages/@rescript/runtime/RescriptTools.res index 1aeac799fe9..032374b6ab2 100644 --- a/packages/@rescript/runtime/RescriptTools.res +++ b/packages/@rescript/runtime/RescriptTools.res @@ -12,5 +12,5 @@ You can use this when you're already running a JS process and want to avoid the let stringifiedJson = ChildProcess.execFileSync(RescriptTools.binaryPath, ["-v"]) ``` */ -@module("#cli/bins") +@module("rescript/cli/bins") external binaryPath: string = "rescript_tools_exe" diff --git a/tests/build_tests/rescript_tools_binary_path/.gitignore b/tests/build_tests/rescript_tools_binary_path/.gitignore new file mode 100644 index 00000000000..93cf72c4033 --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/.gitignore @@ -0,0 +1 @@ +src/**/*.js diff --git a/tests/build_tests/rescript_tools_binary_path/input.js b/tests/build_tests/rescript_tools_binary_path/input.js new file mode 100644 index 00000000000..5785a9af52c --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/input.js @@ -0,0 +1,27 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { rescript_tools_exe } from "#cli/bins"; +import { setup } from "#dev/process"; + +const { execBuildOrThrow, execClean, node } = setup(import.meta.dirname); + +const repoRoot = path.resolve(import.meta.dirname, "..", "..", ".."); +const nodeModules = path.join(import.meta.dirname, "node_modules"); + +await execBuildOrThrow(); + +// The project depends on the rescript package, as a user project does. +await fs.mkdir(nodeModules, { recursive: true }); +await fs.symlink(repoRoot, path.join(nodeModules, "rescript"), "junction"); + +try { + const out = await node(path.join("src", "Main.js")); + assert.equal(out.stderr, ""); + assert.equal(out.stdout.trim(), rescript_tools_exe); +} finally { + await fs.rm(nodeModules, { recursive: true, force: true }); + await execClean(); +} diff --git a/tests/build_tests/rescript_tools_binary_path/rescript.json b/tests/build_tests/rescript_tools_binary_path/rescript.json new file mode 100644 index 00000000000..ae77bf80df1 --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/rescript.json @@ -0,0 +1,8 @@ +{ + "name": "rescript-tools-binary-path", + "sources": "src", + "package-specs": { + "module": "esmodule", + "in-source": true + } +} diff --git a/tests/build_tests/rescript_tools_binary_path/src/Main.res b/tests/build_tests/rescript_tools_binary_path/src/Main.res new file mode 100644 index 00000000000..62c6c0bdb0d --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/src/Main.res @@ -0,0 +1 @@ +Console.log(RescriptTools.binaryPath) diff --git a/tests/build_tests/rescript_tools_binary_path/src/package.json b/tests/build_tests/rescript_tools_binary_path/src/package.json new file mode 100644 index 00000000000..708d119a5f4 --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/src/package.json @@ -0,0 +1,5 @@ +{ + "name": "rescript-tools-binary-path", + "private": true, + "type": "module" +} From e708066ecba7f2ddb7c7d3e7e65384ed8a28a0a2 Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Fri, 2 Oct 2026 08:31:56 +0200 Subject: [PATCH 2/5] Add CHANGELOG entry Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristiano Calcagno --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e74f85413..e6d250c84cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ #### :bug: Bug fix - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 +- Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694 - GenType: type inline-record cases of untagged variants as objects instead of their single field's type. https://github.com/rescript-lang/rescript/pull/8693 - Keep comments on labelled parameters with default values, and before `=?` in optional arrow-type arguments, when formatting. https://github.com/rescript-lang/rescript/pull/8690 - Fix invalid JavaScript that exported names left unbound when a module's toplevel always throws. https://github.com/rescript-lang/rescript/pull/8692 From 11c522d59984389dc8b1a508773274bc5215cf61 Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Fri, 2 Oct 2026 22:07:47 +0200 Subject: [PATCH 3/5] Give rescript/cli/bins a CommonJS entry point CommonJS output compiles RescriptTools.binaryPath to require("rescript/cli/bins"). cli/common/bins.js uses top-level await, so require fails with ERR_REQUIRE_ASYNC_MODULE. The export now maps the require condition to cli/common/bins.cjs, which resolves the platform package synchronously, and keeps bins.js for imports. The build test emits both ES module and CommonJS output and runs each. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristiano Calcagno --- cli/common/bins.cjs | 47 +++++++++++++++++++ package.json | 5 +- packages/artifacts.json | 1 + .../rescript_tools_binary_path/.gitignore | 3 +- .../rescript_tools_binary_path/input.js | 9 ++-- .../rescript_tools_binary_path/rescript.json | 16 +++++-- .../src/package.json | 5 -- 7 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 cli/common/bins.cjs delete mode 100644 tests/build_tests/rescript_tools_binary_path/src/package.json diff --git a/cli/common/bins.cjs b/cli/common/bins.cjs new file mode 100644 index 00000000000..6e3c3a9388f --- /dev/null +++ b/cli/common/bins.cjs @@ -0,0 +1,47 @@ +// @ts-check + +// CommonJS counterpart of bins.js, for `require("rescript/cli/bins")`. +// It locates the platform package with `require.resolve`, so it loads +// synchronously; bins.js uses top-level await, which `require` cannot load. + +const path = require("node:path"); + +const target = `${process.platform}-${process.arch}`; + +const supportedPlatforms = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "win32-x64", +]; + +if (!supportedPlatforms.includes(target)) { + throw new Error(`Platform ${target} is not supported!`); +} + +const binPackageName = `@rescript/${target}`; + +/** @type {string} */ +let binPackageEntry; +try { + binPackageEntry = require.resolve(binPackageName); +} catch { + throw new Error( + `Package ${binPackageName} not found. Make sure the rescript package is installed correctly.`, + ); +} + +// The platform package's entry (bin.js) sits next to its bin directory. +const binDir = path.join(path.dirname(binPackageEntry), "bin"); + +module.exports = { + binDir, + bsc_exe: path.join(binDir, "bsc.exe"), + rescript_editor_analysis_exe: path.join( + binDir, + "rescript-editor-analysis.exe", + ), + rescript_tools_exe: path.join(binDir, "rescript-tools.exe"), + rescript_exe: path.join(binDir, "rescript.exe"), +}; diff --git a/package.json b/package.json index 0114500a8ad..d9894e4f4ae 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,10 @@ "exports": { "./lib/es6/*": "./lib/es6/*", "./lib/js/*": "./lib/js/*", - "./cli/bins": "./cli/common/bins.js", + "./cli/bins": { + "require": "./cli/common/bins.cjs", + "default": "./cli/common/bins.js" + }, "./package.json": "./package.json" }, "imports": { diff --git a/packages/artifacts.json b/packages/artifacts.json index 0145993a6e6..1184c1aa361 100644 --- a/packages/artifacts.json +++ b/packages/artifacts.json @@ -8,6 +8,7 @@ "LICENSE.MIT", "README.md", "cli/bsc.js", + "cli/common/bins.cjs", "cli/common/bins.js", "cli/common/runtime.js", "cli/rescript-tools.js", diff --git a/tests/build_tests/rescript_tools_binary_path/.gitignore b/tests/build_tests/rescript_tools_binary_path/.gitignore index 93cf72c4033..f17f3186b61 100644 --- a/tests/build_tests/rescript_tools_binary_path/.gitignore +++ b/tests/build_tests/rescript_tools_binary_path/.gitignore @@ -1 +1,2 @@ -src/**/*.js +src/**/*.mjs +src/**/*.cjs diff --git a/tests/build_tests/rescript_tools_binary_path/input.js b/tests/build_tests/rescript_tools_binary_path/input.js index 5785a9af52c..3b96635bbaa 100644 --- a/tests/build_tests/rescript_tools_binary_path/input.js +++ b/tests/build_tests/rescript_tools_binary_path/input.js @@ -18,9 +18,12 @@ await fs.mkdir(nodeModules, { recursive: true }); await fs.symlink(repoRoot, path.join(nodeModules, "rescript"), "junction"); try { - const out = await node(path.join("src", "Main.js")); - assert.equal(out.stderr, ""); - assert.equal(out.stdout.trim(), rescript_tools_exe); + // The project emits both ES module and CommonJS output. + for (const output of ["Main.mjs", "Main.cjs"]) { + const out = await node(path.join("src", output)); + assert.equal(out.stderr, "", output); + assert.equal(out.stdout.trim(), rescript_tools_exe, output); + } } finally { await fs.rm(nodeModules, { recursive: true, force: true }); await execClean(); diff --git a/tests/build_tests/rescript_tools_binary_path/rescript.json b/tests/build_tests/rescript_tools_binary_path/rescript.json index ae77bf80df1..95f76309104 100644 --- a/tests/build_tests/rescript_tools_binary_path/rescript.json +++ b/tests/build_tests/rescript_tools_binary_path/rescript.json @@ -1,8 +1,16 @@ { "name": "rescript-tools-binary-path", "sources": "src", - "package-specs": { - "module": "esmodule", - "in-source": true - } + "package-specs": [ + { + "module": "esmodule", + "in-source": true, + "suffix": ".mjs" + }, + { + "module": "commonjs", + "in-source": true, + "suffix": ".cjs" + } + ] } diff --git a/tests/build_tests/rescript_tools_binary_path/src/package.json b/tests/build_tests/rescript_tools_binary_path/src/package.json deleted file mode 100644 index 708d119a5f4..00000000000 --- a/tests/build_tests/rescript_tools_binary_path/src/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "rescript-tools-binary-path", - "private": true, - "type": "module" -} From 58304426e046f84595dfd5541044e80f8dba7438 Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Fri, 2 Oct 2026 22:11:58 +0200 Subject: [PATCH 4/5] Compute the binary paths in bins.cjs only bins.js re-exports the paths from bins.cjs, so ES module and CommonJS consumers share one platform lookup. bins.cjs now also reports an unsupported Node.js version when the platform package is missing. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristiano Calcagno --- cli/common/bins.cjs | 50 +++++++++++++++++++++++-------- cli/common/bins.js | 73 +++++---------------------------------------- 2 files changed, 45 insertions(+), 78 deletions(-) diff --git a/cli/common/bins.cjs b/cli/common/bins.cjs index 6e3c3a9388f..cac1917ce40 100644 --- a/cli/common/bins.cjs +++ b/cli/common/bins.cjs @@ -1,11 +1,13 @@ // @ts-check -// CommonJS counterpart of bins.js, for `require("rescript/cli/bins")`. -// It locates the platform package with `require.resolve`, so it loads -// synchronously; bins.js uses top-level await, which `require` cannot load. +// Paths of the compiler binaries in the platform package (@rescript/). +// The lookup is synchronous so that `require("rescript/cli/bins")` works; +// bins.js re-exports these values for ES modules. const path = require("node:path"); +const minimumNodeVersion = "20.11.0"; + const target = `${process.platform}-${process.arch}`; const supportedPlatforms = [ @@ -27,6 +29,9 @@ let binPackageEntry; try { binPackageEntry = require.resolve(binPackageName); } catch { + // First check if we are on an unsupported node version, as that may be the cause for the error. + checkNodeVersionSupported(); + throw new Error( `Package ${binPackageName} not found. Make sure the rescript package is installed correctly.`, ); @@ -35,13 +40,34 @@ try { // The platform package's entry (bin.js) sits next to its bin directory. const binDir = path.join(path.dirname(binPackageEntry), "bin"); -module.exports = { +exports.binDir = binDir; +exports.bsc_exe = path.join(binDir, "bsc.exe"); +exports.rescript_editor_analysis_exe = path.join( binDir, - bsc_exe: path.join(binDir, "bsc.exe"), - rescript_editor_analysis_exe: path.join( - binDir, - "rescript-editor-analysis.exe", - ), - rescript_tools_exe: path.join(binDir, "rescript-tools.exe"), - rescript_exe: path.join(binDir, "rescript.exe"), -}; + "rescript-editor-analysis.exe", +); +exports.rescript_tools_exe = path.join(binDir, "rescript-tools.exe"); +exports.rescript_exe = path.join(binDir, "rescript.exe"); + +function checkNodeVersionSupported() { + if ( + typeof process !== "undefined" && + process.versions != null && + process.versions.node != null + ) { + const currentVersion = process.versions.node; + const required = minimumNodeVersion.split(".").map(Number); + const current = currentVersion.split(".").map(Number); + if ( + current[0] < required[0] || + (current[0] === required[0] && current[1] < required[1]) || + (current[0] === required[0] && + current[1] === required[1] && + current[2] < required[2]) + ) { + throw new Error( + `ReScript requires Node.js >=${minimumNodeVersion}, but found ${currentVersion}.`, + ); + } + } +} diff --git a/cli/common/bins.js b/cli/common/bins.js index 5800a54f5ea..10a97c2a13a 100644 --- a/cli/common/bins.js +++ b/cli/common/bins.js @@ -1,70 +1,11 @@ // @ts-check -const minimumNodeVersion = "20.11.0"; +// ES module entry for the binary paths computed in bins.cjs. -/** - * @typedef {import("@rescript/linux-x64")} BinaryModuleExports - */ - -const target = `${process.platform}-${process.arch}`; - -const supportedPlatforms = [ - "darwin-arm64", - "darwin-x64", - "linux-arm64", - "linux-x64", - "win32-x64", -]; - -/** @type {BinaryModuleExports} */ -let mod; - -if (supportedPlatforms.includes(target)) { - const binPackageName = `@rescript/${target}`; - - try { - mod = await import(binPackageName); - } catch { - // First check if we are on an unsupported node version, as that may be the cause for the error. - checkNodeVersionSupported(); - - throw new Error( - `Package ${binPackageName} not found. Make sure the rescript package is installed correctly.`, - ); - } -} else { - throw new Error(`Platform ${target} is not supported!`); -} - -export const { +export { binDir, - binPaths: { - bsc_exe, - rescript_editor_analysis_exe, - rescript_tools_exe, - rescript_exe, - }, -} = mod; - -function checkNodeVersionSupported() { - if ( - typeof process !== "undefined" && - process.versions != null && - process.versions.node != null - ) { - const currentVersion = process.versions.node; - const required = minimumNodeVersion.split(".").map(Number); - const current = currentVersion.split(".").map(Number); - if ( - current[0] < required[0] || - (current[0] === required[0] && current[1] < required[1]) || - (current[0] === required[0] && - current[1] === required[1] && - current[2] < required[2]) - ) { - throw new Error( - `ReScript requires Node.js >=${minimumNodeVersion}, but found ${currentVersion}.`, - ); - } - } -} + bsc_exe, + rescript_editor_analysis_exe, + rescript_exe, + rescript_tools_exe, +} from "./bins.cjs"; From 41081717f7c5752d8551ce68b199235acdde953a Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Sun, 4 Oct 2026 09:12:23 +0100 Subject: [PATCH 5/5] Expose only binaryPath through a rescript/tools entry point Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristiano Calcagno --- cli/common/bins.cjs | 4 ++-- cli/common/tools.cjs | 6 ++++++ cli/common/tools.js | 6 ++++++ package.json | 6 +++--- packages/@rescript/runtime/RescriptTools.res | 4 ++-- packages/artifacts.json | 2 ++ 6 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 cli/common/tools.cjs create mode 100644 cli/common/tools.js diff --git a/cli/common/bins.cjs b/cli/common/bins.cjs index cac1917ce40..a4ba4f62dd3 100644 --- a/cli/common/bins.cjs +++ b/cli/common/bins.cjs @@ -1,8 +1,8 @@ // @ts-check // Paths of the compiler binaries in the platform package (@rescript/). -// The lookup is synchronous so that `require("rescript/cli/bins")` works; -// bins.js re-exports these values for ES modules. +// The lookup is synchronous so that tools.cjs, the CommonJS entry of +// `rescript/tools`, can load it; bins.js re-exports these values for ES modules. const path = require("node:path"); diff --git a/cli/common/tools.cjs b/cli/common/tools.cjs new file mode 100644 index 00000000000..daf5359de7f --- /dev/null +++ b/cli/common/tools.cjs @@ -0,0 +1,6 @@ +// @ts-check + +// Public entry point `rescript/tools` for CommonJS: the path of the +// rescript-tools binary, which RescriptTools.binaryPath binds to. + +exports.binaryPath = require("./bins.cjs").rescript_tools_exe; diff --git a/cli/common/tools.js b/cli/common/tools.js new file mode 100644 index 00000000000..12e190ce705 --- /dev/null +++ b/cli/common/tools.js @@ -0,0 +1,6 @@ +// @ts-check + +// Public entry point `rescript/tools` for ES modules: the path of the +// rescript-tools binary, which RescriptTools.binaryPath binds to. + +export { rescript_tools_exe as binaryPath } from "./bins.cjs"; diff --git a/package.json b/package.json index d9894e4f4ae..3033e101c62 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,9 @@ "exports": { "./lib/es6/*": "./lib/es6/*", "./lib/js/*": "./lib/js/*", - "./cli/bins": { - "require": "./cli/common/bins.cjs", - "default": "./cli/common/bins.js" + "./tools": { + "require": "./cli/common/tools.cjs", + "default": "./cli/common/tools.js" }, "./package.json": "./package.json" }, diff --git a/packages/@rescript/runtime/RescriptTools.res b/packages/@rescript/runtime/RescriptTools.res index 032374b6ab2..1e2eb9fc475 100644 --- a/packages/@rescript/runtime/RescriptTools.res +++ b/packages/@rescript/runtime/RescriptTools.res @@ -12,5 +12,5 @@ You can use this when you're already running a JS process and want to avoid the let stringifiedJson = ChildProcess.execFileSync(RescriptTools.binaryPath, ["-v"]) ``` */ -@module("rescript/cli/bins") -external binaryPath: string = "rescript_tools_exe" +@module("rescript/tools") +external binaryPath: string = "binaryPath" diff --git a/packages/artifacts.json b/packages/artifacts.json index 1184c1aa361..088fe2cc8fd 100644 --- a/packages/artifacts.json +++ b/packages/artifacts.json @@ -11,6 +11,8 @@ "cli/common/bins.cjs", "cli/common/bins.js", "cli/common/runtime.js", + "cli/common/tools.cjs", + "cli/common/tools.js", "cli/rescript-tools.js", "cli/rescript.js", "docs/docson/build-schema.json",