diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e74f8541..e6d250c84c 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 diff --git a/cli/common/bins.cjs b/cli/common/bins.cjs new file mode 100644 index 0000000000..a4ba4f62dd --- /dev/null +++ b/cli/common/bins.cjs @@ -0,0 +1,73 @@ +// @ts-check + +// Paths of the compiler binaries in the platform package (@rescript/). +// 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"); + +const minimumNodeVersion = "20.11.0"; + +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 { + // 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.`, + ); +} + +// The platform package's entry (bin.js) sits next to its bin directory. +const binDir = path.join(path.dirname(binPackageEntry), "bin"); + +exports.binDir = binDir; +exports.bsc_exe = path.join(binDir, "bsc.exe"); +exports.rescript_editor_analysis_exe = path.join( + binDir, + "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 5800a54f5e..10a97c2a13 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"; diff --git a/cli/common/tools.cjs b/cli/common/tools.cjs new file mode 100644 index 0000000000..daf5359de7 --- /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 0000000000..12e190ce70 --- /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 a02236f658..3033e101c6 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,10 @@ "exports": { "./lib/es6/*": "./lib/es6/*", "./lib/js/*": "./lib/js/*", + "./tools": { + "require": "./cli/common/tools.cjs", + "default": "./cli/common/tools.js" + }, "./package.json": "./package.json" }, "imports": { diff --git a/packages/@rescript/runtime/RescriptTools.res b/packages/@rescript/runtime/RescriptTools.res index 1aeac799fe..1e2eb9fc47 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") -external binaryPath: string = "rescript_tools_exe" +@module("rescript/tools") +external binaryPath: string = "binaryPath" diff --git a/packages/artifacts.json b/packages/artifacts.json index 0145993a6e..088fe2cc8f 100644 --- a/packages/artifacts.json +++ b/packages/artifacts.json @@ -8,8 +8,11 @@ "LICENSE.MIT", "README.md", "cli/bsc.js", + "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", 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 0000000000..f17f3186b6 --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/.gitignore @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..3b96635bba --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/input.js @@ -0,0 +1,30 @@ +// @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 { + // 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 new file mode 100644 index 0000000000..95f7630910 --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rescript-tools-binary-path", + "sources": "src", + "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/Main.res b/tests/build_tests/rescript_tools_binary_path/src/Main.res new file mode 100644 index 0000000000..62c6c0bdb0 --- /dev/null +++ b/tests/build_tests/rescript_tools_binary_path/src/Main.res @@ -0,0 +1 @@ +Console.log(RescriptTools.binaryPath)