diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c450e7e2..bed0e55169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Playground: Add config options for experimental features and jsx preserve mode. https://github.com/rescript-lang/rescript/pull/7865 - Clean up tests. https://github.com/rescript-lang/rescript/pull/7861 https://github.com/rescript-lang/rescript/pull/7871 +- 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 # 12.0.0-beta.10 diff --git a/analysis/src/ModuleResolution.ml b/analysis/src/ModuleResolution.ml index 504b9a15a8..343e5381d1 100644 --- a/analysis/src/ModuleResolution.ml +++ b/analysis/src/ModuleResolution.ml @@ -3,7 +3,7 @@ let ( /+ ) = Filename.concat let rec resolveNodeModulePath ~startPath name = if name = "@rescript/runtime" then (* Hack: we need a reliable way to resolve modules in monorepos. *) - Some Config.runtime_module_path + Some !Runtime_package.path else let scope = Filename.dirname name in let name = Filename.basename name in diff --git a/biome.json b/biome.json index f68d4cd24b..77c75d6622 100644 --- a/biome.json +++ b/biome.json @@ -75,7 +75,9 @@ "*.d.ts", "*.exe", "package.json", - "packages/artifacts.json" + "packages/artifacts.json", + ".mypy_cache/**", + ".history/**" ] } } diff --git a/cli/bsc.js b/cli/bsc.js index 711ccc17dd..44d3f4ac41 100755 --- a/cli/bsc.js +++ b/cli/bsc.js @@ -5,8 +5,12 @@ import { execFileSync } from "node:child_process"; import { bsc_exe } from "./common/bins.js"; +import { runtimePath } from "./common/runtime.js"; const delegate_args = process.argv.slice(2); +if (!delegate_args.includes("-runtime-path")) { + delegate_args.push("-runtime-path", runtimePath); +} try { execFileSync(bsc_exe, delegate_args, { stdio: "inherit" }); diff --git a/cli/common/bsb.js b/cli/common/bsb.js index 9e94c121dc..8d0cf557ff 100644 --- a/cli/common/bsb.js +++ b/cli/common/bsb.js @@ -6,6 +6,7 @@ import { createServer } from "node:http"; import * as os from "node:os"; import * as path from "node:path"; +import { runtimePath } from "../common/runtime.js"; import { rescript_legacy_exe } from "./bins.js"; import { WebSocket } from "./minisocket.js"; @@ -49,6 +50,11 @@ function acquireBuild(args, options) { if (ownerProcess) { return null; } + + if (args[0] === "build" && !args.includes("-runtime-path")) { + args.push("-runtime-path", runtimePath); + } + try { ownerProcess = child_process.spawn(rescript_legacy_exe, args, { stdio: "inherit", diff --git a/cli/common/runtime.js b/cli/common/runtime.js new file mode 100644 index 0000000000..2b9af59ec0 --- /dev/null +++ b/cli/common/runtime.js @@ -0,0 +1,78 @@ +import { promises as fs } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * 🚨 Why this hack exists: + * + * Unlike Node or Bun, Deno's `import.meta.resolve("npm:...")` does NOT return a + * filesystem path. It just echoes back the npm: specifier. The actual package + * tarballs are unpacked into `node_modules/.deno/...` when you use + * `--node-modules-dir`, and normal `node_modules/` symlinks only exist for + * *direct* dependencies. Transitive deps (like @rescript/runtime in our case) + * only live inside `.deno/` and have no symlink. + * + * Because Deno doesn't expose an API for “give me the absolute path of this npm + * package”, the only way to emulate Node’s/Bun’s `require.resolve` behaviour is + * to glob inside `.deno/` and reconstruct the path manually. + * + * TL;DR: This function exists to compensate for the fact that Deno deliberately hides its + * npm cache layout. If you want a stable on‑disk path for a package in Deno, + * you have to spelunk `node_modules/.deno/>pkg@version>/node_modules/`. + * + * If Deno ever ships a proper API for this, replace this hack immediately. + */ +async function resolvePackageInDeno(pkgName) { + const base = path.resolve("node_modules/.deno"); + const pkgId = pkgName.startsWith("@") ? pkgName.replace("/", "+") : pkgName; + + const { expandGlob } = await import("https://deno.land/std/fs/mod.ts"); + for await (const entry of expandGlob( + path.join(base, `${pkgId}@*/node_modules/${pkgName}`), + )) { + if (entry.isDirectory) { + return await fs.realpath(entry.path); + } + } + + throw new Error( + `Could not resolve ${pkgName} in Deno. Did you enable --node-modules-dir?`, + ); +} + +async function resolvePackageRoot(pkgName) { + const specifier = + typeof globalThis.Deno !== "undefined" + ? `npm:${pkgName}/package.json` + : `${pkgName}/package.json`; + + if (typeof import.meta.resolve === "function") { + const url = import.meta.resolve(specifier); + + if (url.startsWith("file://")) { + // Node & Bun: real local file + const abs = path.dirname(fileURLToPath(url)); + return await fs.realpath(abs); + } + + if (typeof globalThis.Deno !== "undefined") { + return await resolvePackageInDeno(pkgName); + } + + throw new Error( + `Could not resolve ${pkgName} (no physical path available)`, + ); + } + + // Node fallback + const require = createRequire(import.meta.url); + try { + const abs = path.dirname(require.resolve(`${pkgName}/package.json`)); + return await fs.realpath(abs); + } catch { + throw new Error(`Could not resolve ${pkgName} in Node runtime`); + } +} + +export const runtimePath = await resolvePackageRoot("@rescript/runtime"); diff --git a/cli/rescript.js b/cli/rescript.js index 3cce2c9c5a..236e1847e8 100755 --- a/cli/rescript.js +++ b/cli/rescript.js @@ -4,6 +4,7 @@ import * as child_process from "node:child_process"; import { rescript_exe } from "./common/bins.js"; +import { runtimePath } from "./common/runtime.js"; const args = process.argv.slice(2); @@ -18,6 +19,7 @@ const args = process.argv.slice(2); // exit the parent with the correct status only after the child has exited. const child = child_process.spawn(rescript_exe, args, { stdio: "inherit", + env: { ...process.env, RESCRIPT_RUNTIME: runtimePath }, }); // Map POSIX signal names to conventional exit status numbers so we can diff --git a/compiler/bsb/bsb_arg.ml b/compiler/bsb/bsb_arg.ml index b0adc252f6..aa7183e5a7 100644 --- a/compiler/bsb/bsb_arg.ml +++ b/compiler/bsb/bsb_arg.ml @@ -48,8 +48,11 @@ let usage_b (buf : Ext_buffer.t) ~usage (speclist : t) = else ( buf +> "\nOptions:\n"; let max_col = ref 0 in - Ext_array.iter speclist (fun (key, _, _) -> - if String.length key > !max_col then max_col := String.length key); + Ext_array.iter speclist (fun (key, _, doc) -> + if + (not (Ext_string.starts_with doc "*internal*")) + && String.length key > !max_col + then max_col := String.length key); Ext_array.iter speclist (fun (key, _, doc) -> if not (Ext_string.starts_with doc "*internal*") then ( buf +> " "; diff --git a/compiler/bsb/bsb_exception.ml b/compiler/bsb/bsb_exception.ml index eecb9dd03f..121e902bb6 100644 --- a/compiler/bsb/bsb_exception.ml +++ b/compiler/bsb/bsb_exception.ml @@ -47,7 +47,7 @@ let print (fmt : Format.formatter) (x : error) = modname | Package_not_found name -> let name = Bsb_pkg_types.to_string name in - if Ext_string.equal name Bs_version.package_name then + if Ext_string.equal name Runtime_package.name then Format.fprintf fmt "File \"rescript.json\", line 1\n\ @{Error:@} package @{%s@} is not found\n\ diff --git a/compiler/bsb/bsb_ninja_rule.ml b/compiler/bsb/bsb_ninja_rule.ml index f28dd5b53c..2f90bb42e9 100644 --- a/compiler/bsb/bsb_ninja_rule.ml +++ b/compiler/bsb/bsb_ninja_rule.ml @@ -107,6 +107,7 @@ let make_custom_rules ~(gentype_config : Bsb_config_types.gentype_config) string = Ext_buffer.clear buf; Ext_buffer.add_string buf bsc; + Ext_buffer.add_string buf (" -runtime-path " ^ !Runtime_package.path); Ext_buffer.add_string buf ns_flag; if read_cmi = `yes then Ext_buffer.add_string buf " -bs-read-cmi"; (* The include order matters below *) @@ -139,6 +140,7 @@ let make_custom_rules ~(gentype_config : Bsb_config_types.gentype_config) let mk_ast = Ext_buffer.clear buf; Ext_buffer.add_string buf bsc; + Ext_buffer.add_string buf (" -runtime-path " ^ !Runtime_package.path); Ext_buffer.add_char_string buf ' ' warnings; (match ppx_files with | [] -> () diff --git a/compiler/bsb_exe/rescript_main.ml b/compiler/bsb_exe/rescript_main.ml index 48c33f4236..b28527a01d 100644 --- a/compiler/bsb_exe/rescript_main.ml +++ b/compiler/bsb_exe/rescript_main.ml @@ -110,6 +110,8 @@ let install_target () = let eid = Bsb_unix.run_command_execv install_command in if eid <> 0 then Bsb_unix.command_fatal_error install_command eid +let setup_runtime_path path = Runtime_package.path := path + let build_subcommand ~start argv argv_len = let i = Ext_array.rfind_with_index argv Ext_string.equal separator in @@ -141,6 +143,9 @@ let build_subcommand ~start argv argv_len = unit_set_spec no_deps_mode, "*internal* Needed for watcher to build without dependencies on file \ change" ); + ( "-runtime-path", + string_call setup_runtime_path, + "*internal* Set the path of the runtime package (@rescript/runtime)" ); ( "-warn-error", string_call (fun s -> warning_as_error := Some s), "Warning numbers and whether to turn them into errors, e.g., \ diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index 43667b184e..ec40263bb6 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -50,6 +50,8 @@ let set_abs_input_name sourcefile = sourcefile let setup_outcome_printer () = Lazy.force Res_outcome_printer.setup +let setup_runtime_path path = Runtime_package.path := path + let process_file sourcefile ?kind ppf = (* This is a better default then "", it will be changed later The {!Location.input_name} relies on that we write the binary ast @@ -394,6 +396,9 @@ let command_line_flags : (string * Bsc_args.spec * string) array = ( "-unsafe", set Clflags.fast, "*internal* Do not compile bounds checking on array and string access" ); + ( "-runtime-path", + string_call setup_runtime_path, + "*internal* Set the path of the runtime package (@rescript/runtime)" ); ( "-warn-help", unit_call Warnings.help_warnings, "Show description of warning numbers" ); diff --git a/compiler/common/bs_version.ml b/compiler/common/bs_version.ml index d6b8241c4e..ecd302f740 100644 --- a/compiler/common/bs_version.ml +++ b/compiler/common/bs_version.ml @@ -23,4 +23,3 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *) let version = "12.0.0-beta.11" let header = "// Generated by ReScript, PLEASE EDIT WITH CARE" -let package_name = "@rescript/runtime" diff --git a/compiler/common/bs_version.mli b/compiler/common/bs_version.mli index b18b6316c0..4ad826ea25 100644 --- a/compiler/common/bs_version.mli +++ b/compiler/common/bs_version.mli @@ -25,5 +25,3 @@ val version : string val header : string - -val package_name : string diff --git a/compiler/core/js_name_of_module_id.ml b/compiler/core/js_name_of_module_id.ml index e14ea15c00..1d6f30190c 100644 --- a/compiler/core/js_name_of_module_id.ml +++ b/compiler/core/js_name_of_module_id.ml @@ -95,7 +95,7 @@ let get_runtime_module_path (*Invariant: the package path to rescript, it is used to calculate relative js path *) - (Config.runtime_module_path // dep_path // js_file) + (!Runtime_package.path // dep_path // js_file) (* [output_dir] is decided by the command line argument *) let string_of_module_id diff --git a/compiler/core/js_packages_info.ml b/compiler/core/js_packages_info.ml index d490a5050c..d181b6e086 100644 --- a/compiler/core/js_packages_info.ml +++ b/compiler/core/js_packages_info.ml @@ -48,7 +48,7 @@ let runtime_dir_of_module_system (ms : module_system) = | Esmodule | Es6_global -> "es6" let runtime_package_path (ms : module_system) js_file = - Bs_version.package_name // "lib" // runtime_dir_of_module_system ms // js_file + Runtime_package.name // "lib" // runtime_dir_of_module_system ms // js_file type t = {name: package_name; module_systems: package_info list} @@ -163,7 +163,7 @@ let query_package_infos ({name; module_systems} : t) with | Some k -> let rel_path = k.path in - let pkg_rel_path = Bs_version.package_name // rel_path in + let pkg_rel_path = Runtime_package.name // rel_path in Package_found {rel_path; pkg_rel_path; suffix = k.suffix} | None -> Package_not_found) diff --git a/compiler/core/res_compmisc.ml b/compiler/core/res_compmisc.ml index 3f21ce9cc5..804b3fc7cd 100644 --- a/compiler/core/res_compmisc.ml +++ b/compiler/core/res_compmisc.ml @@ -23,13 +23,15 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *) let init_path () = - let dirs = !Clflags.include_dirs in - let exp_dirs = - List.map (Misc.expand_directory Config.standard_library) dirs + let stdlib_dir = + let ( // ) = Filename.concat in + !Runtime_package.path // "lib" // "ocaml" in + let dirs = !Clflags.include_dirs in + let exp_dirs = List.map (Misc.expand_directory stdlib_dir) dirs in Config.load_path := if !Js_config.no_stdlib then exp_dirs - else List.rev_append exp_dirs [Config.standard_library]; + else List.rev_append exp_dirs [stdlib_dir]; Env.reset_cache () (* Return the initial environment in which compilation proceeds. *) diff --git a/compiler/ext/config.ml b/compiler/ext/config.ml index 9cb5f6f25e..d9f7bb6425 100644 --- a/compiler/ext/config.ml +++ b/compiler/ext/config.ml @@ -1,40 +1,3 @@ -(* This resolves the location of the standard library starting from the location of bsc.exe - (@rescript/{platform}/bin/bsc.exe), handling different supported package layouts. *) -let runtime_module_path = - let build_path rest path = - String.concat Filename.dir_sep (List.rev_append rest path) - in - match - Sys.executable_name |> Filename.dirname - |> String.split_on_char Filename.dir_sep.[0] - |> List.rev - with - (* 1. Packages installed via pnpm - - bin: node_modules/.pnpm/@rescript+darwin-arm64@12.0.0-alpha.13/node_modules/@rescript/darwin-arm64/bin - - runtime: node_modules/.pnpm/node_modules/@rescript/runtime (symlink) - *) - | "bin" :: _platform :: "@rescript" :: "node_modules" :: _package :: ".pnpm" - :: "node_modules" :: rest -> - build_path rest - ["node_modules"; ".pnpm"; "node_modules"; "@rescript"; "runtime"] - (* 2. Packages installed via npm - - bin: node_modules/@rescript/{platform}/bin - - runtime: node_modules/@rescript/runtime - *) - | "bin" :: _platform :: "@rescript" :: "node_modules" :: rest -> - build_path rest ["node_modules"; "@rescript"; "runtime"] - (* 3. Several other cases that can occur in local development, e.g. - - bin: /packages/@rescript/{platform}/bin, /_build/install/default/bin - - runtime: /packages/@rescript/runtime - *) - | _ :: _ :: _ :: _ :: rest -> - build_path rest ["packages"; "@rescript"; "runtime"] - | _ -> "" - -let standard_library = - let ( // ) = Filename.concat in - runtime_module_path // "lib" // "ocaml" - let cmi_magic_number = "Caml1999I022" and ast_impl_magic_number = "Caml1999M022" diff --git a/compiler/ext/config.mli b/compiler/ext/config.mli index 187803255e..fe13a03c99 100644 --- a/compiler/ext/config.mli +++ b/compiler/ext/config.mli @@ -15,12 +15,6 @@ (* System configuration *) -(* The directory containing the runtime module (@rescript/runtime) *) -val runtime_module_path : string - -(* The directory containing the runtime artifacts (@rescript/runtime/lib/ocaml) *) -val standard_library : string - (* Directories in the search path for .cmi and .cmo files *) val load_path : string list ref diff --git a/compiler/ext/runtime_package.ml b/compiler/ext/runtime_package.ml new file mode 100644 index 0000000000..34d4afd0bd --- /dev/null +++ b/compiler/ext/runtime_package.ml @@ -0,0 +1,29 @@ +let name = "@rescript/runtime" + +(* Simple default approach to find the runtime package path. This will not work with all package managers/layouts. *) +let default_path = + let build_path rest path = + String.concat Filename.dir_sep (List.rev_append rest path) + in + match + Sys.executable_name |> Filename.dirname + |> String.split_on_char Filename.dir_sep.[0] + |> List.rev + with + (* 1. Packages installed via npm + - bin: node_modules/@rescript/{platform}/bin + - runtime: node_modules/@rescript/runtime + *) + | "bin" :: _platform :: "@rescript" :: "node_modules" :: rest -> + build_path rest ["node_modules"; "@rescript"; "runtime"] + (* 2. Several other cases that can occur in local development, e.g. + - bin: /packages/@rescript/{platform}/bin, /_build/install/default/bin + - runtime: /packages/@rescript/runtime + *) + | _ :: _ :: _ :: _ :: rest -> + build_path rest ["packages"; "@rescript"; "runtime"] + | _ -> "" + +(* To support pnpm and other package managers/layouts, we determine the path on the JS side and pass it in +via -runtime-path to override the default. *) +let path = ref default_path diff --git a/compiler/ext/runtime_package.mli b/compiler/ext/runtime_package.mli new file mode 100644 index 0000000000..cf7cc9f4d7 --- /dev/null +++ b/compiler/ext/runtime_package.mli @@ -0,0 +1,2 @@ +val name : string +val path : string ref diff --git a/packages/artifacts.json b/packages/artifacts.json index b156f8d13d..dddbaa0eb4 100644 --- a/packages/artifacts.json +++ b/packages/artifacts.json @@ -12,6 +12,7 @@ "cli/common/bins.js", "cli/common/bsb.js", "cli/common/minisocket.js", + "cli/common/runtime.js", "cli/rescript-legacy.js", "cli/rescript-legacy/dump.js", "cli/rescript-legacy/format.js", diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index d0b89b7110..1a93b2bf36 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -99,7 +99,7 @@ pub fn get_compiler_args(rescript_file_path: &Path) -> Result { &None, is_type_dev, true, - ); + )?; let result = serde_json::to_string_pretty(&CompilerArgs { compiler_args, diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index f5b908ea11..578ce4c112 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -6,6 +6,7 @@ use super::build_types::*; use super::logs; use super::packages; use crate::config; +use crate::config::Config; use crate::helpers; use crate::helpers::StrippedVerbatimPath; use crate::project_context::ProjectContext; @@ -335,6 +336,21 @@ pub fn compile( Ok((compile_errors, compile_warnings, num_compiled_modules)) } +fn get_runtime_path_args(package_config: &Config, project_context: &ProjectContext) -> Result> { + match std::env::var("RESCRIPT_RUNTIME") { + Ok(runtime_path) => Ok(vec!["-runtime-path".to_string(), runtime_path]), + Err(_) => match helpers::try_package_path(package_config, project_context, "@rescript/runtime") { + Ok(runtime_path) => Ok(vec![ + "-runtime-path".to_string(), + runtime_path.to_string_lossy().to_string(), + ]), + Err(err) => Err(anyhow!( + "The rescript runtime package could not be found.\nPlease set RESCRIPT_RUNTIME environment variable or make sure the runtime package is installed.\nError: {err}" + )), + }, + } +} + pub fn compiler_args( config: &config::Config, ast_path: &Path, @@ -349,7 +365,7 @@ pub fn compiler_args( // Is the file listed as "type":"dev"? is_type_dev: bool, is_local_dep: bool, -) -> Vec { +) -> Result> { let bsc_flags = config::flatten_flags(&config.compiler_flags); let dependency_paths = get_dependency_paths(config, project_context, packages, is_type_dev); let module_name = helpers::file_path_to_module_name(file_path, &config.get_namespace()); @@ -431,13 +447,16 @@ pub fn compiler_args( .collect() }; - vec![ + let runtime_path_args = get_runtime_path_args(config, project_context)?; + + Ok(vec![ namespace_args, read_cmi_args, vec![ "-I".to_string(), Path::new("..").join("ocaml").to_string_lossy().to_string(), ], + runtime_path_args, dependency_paths, jsx_args, jsx_module_args, @@ -460,7 +479,7 @@ pub fn compiler_args( // ], vec![ast_path.to_string_lossy().to_string()], ] - .concat() + .concat()) } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -588,7 +607,7 @@ fn compile_file( &Some(packages), is_type_dev, package.is_local_dep, - ); + )?; let to_mjs = Command::new(bsc_path) .current_dir( diff --git a/rewatch/tests/get_bin_paths.js b/rewatch/tests/get_bin_paths.js index 31a6e384fe..1d52447282 100644 --- a/rewatch/tests/get_bin_paths.js +++ b/rewatch/tests/get_bin_paths.js @@ -1,4 +1,15 @@ // @ts-check -import { bsc_exe } from '../../cli/common/bins.js'; +import { bsc_exe } from "../../cli/common/bins.js"; +import path from "node:path"; + +const runtimePath = path.resolve( + import.meta.dirname, + "..", + "..", + "packages", + "@rescript", + "runtime" +); console.log(`RESCRIPT_BSC_EXE='${bsc_exe}'`); +console.log(`RESCRIPT_RUNTIME='${runtimePath}'`); diff --git a/rewatch/tests/suite-ci.sh b/rewatch/tests/suite-ci.sh index d7da7de3ce..c15a3d823d 100755 --- a/rewatch/tests/suite-ci.sh +++ b/rewatch/tests/suite-ci.sh @@ -15,6 +15,7 @@ fi export REWATCH_EXECUTABLE export RESCRIPT_BSC_EXE +export RESCRIPT_RUNTIME source ./utils.sh