Skip to content

Commit 4544634

Browse files
cristianocclaude
andauthored
Resolve RescriptTools.binaryPath in user projects (#8694)
* 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 <noreply@anthropic.com> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> * Add CHANGELOG entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> * 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 <noreply@anthropic.com> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> * 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 <noreply@anthropic.com> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> * Expose only binaryPath through a rescript/tools entry point Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> --------- Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a55e289 commit 4544634

12 files changed

Lines changed: 151 additions & 68 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#### :bug: Bug fix
2222

2323
- 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
24+
- Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694
2425
- 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
2526
- 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
2627
- Fix invalid JavaScript that exported names left unbound when a module's toplevel always throws. https://github.com/rescript-lang/rescript/pull/8692

‎cli/common/bins.cjs‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// @ts-check
2+
3+
// Paths of the compiler binaries in the platform package (@rescript/<target>).
4+
// The lookup is synchronous so that tools.cjs, the CommonJS entry of
5+
// `rescript/tools`, can load it; bins.js re-exports these values for ES modules.
6+
7+
const path = require("node:path");
8+
9+
const minimumNodeVersion = "20.11.0";
10+
11+
const target = `${process.platform}-${process.arch}`;
12+
13+
const supportedPlatforms = [
14+
"darwin-arm64",
15+
"darwin-x64",
16+
"linux-arm64",
17+
"linux-x64",
18+
"win32-x64",
19+
];
20+
21+
if (!supportedPlatforms.includes(target)) {
22+
throw new Error(`Platform ${target} is not supported!`);
23+
}
24+
25+
const binPackageName = `@rescript/${target}`;
26+
27+
/** @type {string} */
28+
let binPackageEntry;
29+
try {
30+
binPackageEntry = require.resolve(binPackageName);
31+
} catch {
32+
// First check if we are on an unsupported node version, as that may be the cause for the error.
33+
checkNodeVersionSupported();
34+
35+
throw new Error(
36+
`Package ${binPackageName} not found. Make sure the rescript package is installed correctly.`,
37+
);
38+
}
39+
40+
// The platform package's entry (bin.js) sits next to its bin directory.
41+
const binDir = path.join(path.dirname(binPackageEntry), "bin");
42+
43+
exports.binDir = binDir;
44+
exports.bsc_exe = path.join(binDir, "bsc.exe");
45+
exports.rescript_editor_analysis_exe = path.join(
46+
binDir,
47+
"rescript-editor-analysis.exe",
48+
);
49+
exports.rescript_tools_exe = path.join(binDir, "rescript-tools.exe");
50+
exports.rescript_exe = path.join(binDir, "rescript.exe");
51+
52+
function checkNodeVersionSupported() {
53+
if (
54+
typeof process !== "undefined" &&
55+
process.versions != null &&
56+
process.versions.node != null
57+
) {
58+
const currentVersion = process.versions.node;
59+
const required = minimumNodeVersion.split(".").map(Number);
60+
const current = currentVersion.split(".").map(Number);
61+
if (
62+
current[0] < required[0] ||
63+
(current[0] === required[0] && current[1] < required[1]) ||
64+
(current[0] === required[0] &&
65+
current[1] === required[1] &&
66+
current[2] < required[2])
67+
) {
68+
throw new Error(
69+
`ReScript requires Node.js >=${minimumNodeVersion}, but found ${currentVersion}.`,
70+
);
71+
}
72+
}
73+
}

‎cli/common/bins.js‎

Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,11 @@
11
// @ts-check
22

3-
const minimumNodeVersion = "20.11.0";
3+
// ES module entry for the binary paths computed in bins.cjs.
44

5-
/**
6-
* @typedef {import("@rescript/linux-x64")} BinaryModuleExports
7-
*/
8-
9-
const target = `${process.platform}-${process.arch}`;
10-
11-
const supportedPlatforms = [
12-
"darwin-arm64",
13-
"darwin-x64",
14-
"linux-arm64",
15-
"linux-x64",
16-
"win32-x64",
17-
];
18-
19-
/** @type {BinaryModuleExports} */
20-
let mod;
21-
22-
if (supportedPlatforms.includes(target)) {
23-
const binPackageName = `@rescript/${target}`;
24-
25-
try {
26-
mod = await import(binPackageName);
27-
} catch {
28-
// First check if we are on an unsupported node version, as that may be the cause for the error.
29-
checkNodeVersionSupported();
30-
31-
throw new Error(
32-
`Package ${binPackageName} not found. Make sure the rescript package is installed correctly.`,
33-
);
34-
}
35-
} else {
36-
throw new Error(`Platform ${target} is not supported!`);
37-
}
38-
39-
export const {
5+
export {
406
binDir,
41-
binPaths: {
42-
bsc_exe,
43-
rescript_editor_analysis_exe,
44-
rescript_tools_exe,
45-
rescript_exe,
46-
},
47-
} = mod;
48-
49-
function checkNodeVersionSupported() {
50-
if (
51-
typeof process !== "undefined" &&
52-
process.versions != null &&
53-
process.versions.node != null
54-
) {
55-
const currentVersion = process.versions.node;
56-
const required = minimumNodeVersion.split(".").map(Number);
57-
const current = currentVersion.split(".").map(Number);
58-
if (
59-
current[0] < required[0] ||
60-
(current[0] === required[0] && current[1] < required[1]) ||
61-
(current[0] === required[0] &&
62-
current[1] === required[1] &&
63-
current[2] < required[2])
64-
) {
65-
throw new Error(
66-
`ReScript requires Node.js >=${minimumNodeVersion}, but found ${currentVersion}.`,
67-
);
68-
}
69-
}
70-
}
7+
bsc_exe,
8+
rescript_editor_analysis_exe,
9+
rescript_exe,
10+
rescript_tools_exe,
11+
} from "./bins.cjs";

‎cli/common/tools.cjs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
3+
// Public entry point `rescript/tools` for CommonJS: the path of the
4+
// rescript-tools binary, which RescriptTools.binaryPath binds to.
5+
6+
exports.binaryPath = require("./bins.cjs").rescript_tools_exe;

‎cli/common/tools.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
3+
// Public entry point `rescript/tools` for ES modules: the path of the
4+
// rescript-tools binary, which RescriptTools.binaryPath binds to.
5+
6+
export { rescript_tools_exe as binaryPath } from "./bins.cjs";

‎package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"exports": {
7070
"./lib/es6/*": "./lib/es6/*",
7171
"./lib/js/*": "./lib/js/*",
72+
"./tools": {
73+
"require": "./cli/common/tools.cjs",
74+
"default": "./cli/common/tools.js"
75+
},
7276
"./package.json": "./package.json"
7377
},
7478
"imports": {

‎packages/@rescript/runtime/RescriptTools.res‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ You can use this when you're already running a JS process and want to avoid the
1212
let stringifiedJson = ChildProcess.execFileSync(RescriptTools.binaryPath, ["-v"])
1313
```
1414
*/
15-
@module("#cli/bins")
16-
external binaryPath: string = "rescript_tools_exe"
15+
@module("rescript/tools")
16+
external binaryPath: string = "binaryPath"

‎packages/artifacts.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
"LICENSE.MIT",
99
"README.md",
1010
"cli/bsc.js",
11+
"cli/common/bins.cjs",
1112
"cli/common/bins.js",
1213
"cli/common/runtime.js",
14+
"cli/common/tools.cjs",
15+
"cli/common/tools.js",
1316
"cli/rescript-tools.js",
1417
"cli/rescript.js",
1518
"docs/docson/build-schema.json",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
src/**/*.mjs
2+
src/**/*.cjs
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// @ts-check
2+
3+
import * as assert from "node:assert";
4+
import * as fs from "node:fs/promises";
5+
import * as path from "node:path";
6+
import { rescript_tools_exe } from "#cli/bins";
7+
import { setup } from "#dev/process";
8+
9+
const { execBuildOrThrow, execClean, node } = setup(import.meta.dirname);
10+
11+
const repoRoot = path.resolve(import.meta.dirname, "..", "..", "..");
12+
const nodeModules = path.join(import.meta.dirname, "node_modules");
13+
14+
await execBuildOrThrow();
15+
16+
// The project depends on the rescript package, as a user project does.
17+
await fs.mkdir(nodeModules, { recursive: true });
18+
await fs.symlink(repoRoot, path.join(nodeModules, "rescript"), "junction");
19+
20+
try {
21+
// The project emits both ES module and CommonJS output.
22+
for (const output of ["Main.mjs", "Main.cjs"]) {
23+
const out = await node(path.join("src", output));
24+
assert.equal(out.stderr, "", output);
25+
assert.equal(out.stdout.trim(), rescript_tools_exe, output);
26+
}
27+
} finally {
28+
await fs.rm(nodeModules, { recursive: true, force: true });
29+
await execClean();
30+
}

0 commit comments

Comments
 (0)