Skip to content

Commit dee0465

Browse files
authored
Resolve @rescript/runtime (#7858)
* Initial test setup Good old windows compile remove foo * Undo unwanted stuff * biome... * Add deno sample * Compile again * Add webapi to deno sample * Failing tests, runtime not found. * ignore history folder * Add -runtime-path * Resolve runtime via helper * Remove -nostdlib from test projects * Pass rescript runtime as environment variable * Consume RESCRIPT_RUNTIME * Test projects now consume rescript/runtime * Add RESCRIPT_RUNTIME to bash tests * Use actual deno to invoke tests * Add resolvePackageRoot to artifacts file * Don't run tests in parallel because of git index conflicts * Comment out failing test for now * Include runtime argument in bsb.js * ignore all lib/bs * Add runtime as well to bsc.js * Add -runtime-path to bsb as well. * Renamed artifact * Remove new rewatch tests * Revert yarn.lock * Add changelog entry
1 parent 1cd4744 commit dee0465

26 files changed

+191
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
- Playground: Add config options for experimental features and jsx preserve mode. https://github.com/rescript-lang/rescript/pull/7865
4343
- Clean up tests. https://github.com/rescript-lang/rescript/pull/7861 https://github.com/rescript-lang/rescript/pull/7871
44+
- Add `-runtime-path` flag to `bsc` (and `bsb`), we are detecting the location of `@rescript/runtime` in `cli/rescript.js` based on runtime module resolution. https://github.com/rescript-lang/rescript/pull/7858
4445

4546
# 12.0.0-beta.10
4647

analysis/src/ModuleResolution.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ let ( /+ ) = Filename.concat
33
let rec resolveNodeModulePath ~startPath name =
44
if name = "@rescript/runtime" then
55
(* Hack: we need a reliable way to resolve modules in monorepos. *)
6-
Some Config.runtime_module_path
6+
Some !Runtime_package.path
77
else
88
let scope = Filename.dirname name in
99
let name = Filename.basename name in

biome.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@
7575
"*.d.ts",
7676
"*.exe",
7777
"package.json",
78-
"packages/artifacts.json"
78+
"packages/artifacts.json",
79+
".mypy_cache/**",
80+
".history/**"
7981
]
8082
}
8183
}

cli/bsc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
import { execFileSync } from "node:child_process";
66

77
import { bsc_exe } from "./common/bins.js";
8+
import { runtimePath } from "./common/runtime.js";
89

910
const delegate_args = process.argv.slice(2);
11+
if (!delegate_args.includes("-runtime-path")) {
12+
delegate_args.push("-runtime-path", runtimePath);
13+
}
1014

1115
try {
1216
execFileSync(bsc_exe, delegate_args, { stdio: "inherit" });

cli/common/bsb.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createServer } from "node:http";
66
import * as os from "node:os";
77
import * as path from "node:path";
88

9+
import { runtimePath } from "../common/runtime.js";
910
import { rescript_legacy_exe } from "./bins.js";
1011
import { WebSocket } from "./minisocket.js";
1112

@@ -49,6 +50,11 @@ function acquireBuild(args, options) {
4950
if (ownerProcess) {
5051
return null;
5152
}
53+
54+
if (args[0] === "build" && !args.includes("-runtime-path")) {
55+
args.push("-runtime-path", runtimePath);
56+
}
57+
5258
try {
5359
ownerProcess = child_process.spawn(rescript_legacy_exe, args, {
5460
stdio: "inherit",

cli/common/runtime.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { promises as fs } from "node:fs";
2+
import { createRequire } from "node:module";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
/**
7+
* 🚨 Why this hack exists:
8+
*
9+
* Unlike Node or Bun, Deno's `import.meta.resolve("npm:...")` does NOT return a
10+
* filesystem path. It just echoes back the npm: specifier. The actual package
11+
* tarballs are unpacked into `node_modules/.deno/...` when you use
12+
* `--node-modules-dir`, and normal `node_modules/<pkg>` symlinks only exist for
13+
* *direct* dependencies. Transitive deps (like @rescript/runtime in our case)
14+
* only live inside `.deno/` and have no symlink.
15+
*
16+
* Because Deno doesn't expose an API for “give me the absolute path of this npm
17+
* package”, the only way to emulate Node’s/Bun’s `require.resolve` behaviour is
18+
* to glob inside `.deno/` and reconstruct the path manually.
19+
*
20+
* TL;DR: This function exists to compensate for the fact that Deno deliberately hides its
21+
* npm cache layout. If you want a stable on‑disk path for a package in Deno,
22+
* you have to spelunk `node_modules/.deno/>pkg@version>/node_modules/<pkg>`.
23+
*
24+
* If Deno ever ships a proper API for this, replace this hack immediately.
25+
*/
26+
async function resolvePackageInDeno(pkgName) {
27+
const base = path.resolve("node_modules/.deno");
28+
const pkgId = pkgName.startsWith("@") ? pkgName.replace("/", "+") : pkgName;
29+
30+
const { expandGlob } = await import("https://deno.land/std/fs/mod.ts");
31+
for await (const entry of expandGlob(
32+
path.join(base, `${pkgId}@*/node_modules/${pkgName}`),
33+
)) {
34+
if (entry.isDirectory) {
35+
return await fs.realpath(entry.path);
36+
}
37+
}
38+
39+
throw new Error(
40+
`Could not resolve ${pkgName} in Deno. Did you enable --node-modules-dir?`,
41+
);
42+
}
43+
44+
async function resolvePackageRoot(pkgName) {
45+
const specifier =
46+
typeof globalThis.Deno !== "undefined"
47+
? `npm:${pkgName}/package.json`
48+
: `${pkgName}/package.json`;
49+
50+
if (typeof import.meta.resolve === "function") {
51+
const url = import.meta.resolve(specifier);
52+
53+
if (url.startsWith("file://")) {
54+
// Node & Bun: real local file
55+
const abs = path.dirname(fileURLToPath(url));
56+
return await fs.realpath(abs);
57+
}
58+
59+
if (typeof globalThis.Deno !== "undefined") {
60+
return await resolvePackageInDeno(pkgName);
61+
}
62+
63+
throw new Error(
64+
`Could not resolve ${pkgName} (no physical path available)`,
65+
);
66+
}
67+
68+
// Node fallback
69+
const require = createRequire(import.meta.url);
70+
try {
71+
const abs = path.dirname(require.resolve(`${pkgName}/package.json`));
72+
return await fs.realpath(abs);
73+
} catch {
74+
throw new Error(`Could not resolve ${pkgName} in Node runtime`);
75+
}
76+
}
77+
78+
export const runtimePath = await resolvePackageRoot("@rescript/runtime");

cli/rescript.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import * as child_process from "node:child_process";
66
import { rescript_exe } from "./common/bins.js";
7+
import { runtimePath } from "./common/runtime.js";
78

89
const args = process.argv.slice(2);
910

@@ -18,6 +19,7 @@ const args = process.argv.slice(2);
1819
// exit the parent with the correct status only after the child has exited.
1920
const child = child_process.spawn(rescript_exe, args, {
2021
stdio: "inherit",
22+
env: { ...process.env, RESCRIPT_RUNTIME: runtimePath },
2123
});
2224

2325
// Map POSIX signal names to conventional exit status numbers so we can

compiler/bsb/bsb_arg.ml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ let usage_b (buf : Ext_buffer.t) ~usage (speclist : t) =
4848
else (
4949
buf +> "\nOptions:\n";
5050
let max_col = ref 0 in
51-
Ext_array.iter speclist (fun (key, _, _) ->
52-
if String.length key > !max_col then max_col := String.length key);
51+
Ext_array.iter speclist (fun (key, _, doc) ->
52+
if
53+
(not (Ext_string.starts_with doc "*internal*"))
54+
&& String.length key > !max_col
55+
then max_col := String.length key);
5356
Ext_array.iter speclist (fun (key, _, doc) ->
5457
if not (Ext_string.starts_with doc "*internal*") then (
5558
buf +> " ";

compiler/bsb/bsb_exception.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ let print (fmt : Format.formatter) (x : error) =
4747
modname
4848
| Package_not_found name ->
4949
let name = Bsb_pkg_types.to_string name in
50-
if Ext_string.equal name Bs_version.package_name then
50+
if Ext_string.equal name Runtime_package.name then
5151
Format.fprintf fmt
5252
"File \"rescript.json\", line 1\n\
5353
@{<error>Error:@} package @{<error>%s@} is not found\n\

compiler/bsb/bsb_ninja_rule.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ let make_custom_rules ~(gentype_config : Bsb_config_types.gentype_config)
107107
string =
108108
Ext_buffer.clear buf;
109109
Ext_buffer.add_string buf bsc;
110+
Ext_buffer.add_string buf (" -runtime-path " ^ !Runtime_package.path);
110111
Ext_buffer.add_string buf ns_flag;
111112
if read_cmi = `yes then Ext_buffer.add_string buf " -bs-read-cmi";
112113
(* The include order matters below *)
@@ -139,6 +140,7 @@ let make_custom_rules ~(gentype_config : Bsb_config_types.gentype_config)
139140
let mk_ast =
140141
Ext_buffer.clear buf;
141142
Ext_buffer.add_string buf bsc;
143+
Ext_buffer.add_string buf (" -runtime-path " ^ !Runtime_package.path);
142144
Ext_buffer.add_char_string buf ' ' warnings;
143145
(match ppx_files with
144146
| [] -> ()

0 commit comments

Comments
 (0)