Skip to content

Commit 957acbf

Browse files
committed
Fix JavaScript import paths for files in subdirectories after splitting -bs-package-output
1 parent 6d4c90f commit 957acbf

File tree

3 files changed

+135
-12
lines changed

3 files changed

+135
-12
lines changed

compiler/bsb/bsb_package_specs.ml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,17 +132,30 @@ let from_json suffix (x : Ext_json_types.t) : Spec_set.t =
132132

133133
[@@@warning "+9"]
134134

135-
let package_flag ({format; in_source; suffix} : Bsb_spec_set.spec) _dir =
136-
(* Generate separate flags for module system, suffix, and output path *)
135+
let package_flag ({format; in_source; suffix} : Bsb_spec_set.spec) dir =
136+
(* Generates three separate compiler flags that were split from the original
137+
single "-bs-package-output module:path:suffix" flag.
138+
139+
The 'dir' parameter comes from ninja's $in_d variable, which expands to the
140+
directory containing the source file being compiled. For example:
141+
- File: src/core/intl/Module.res -> dir = "src/core/intl"
142+
- File: src/core/Module.res -> dir = "src/core"
143+
144+
Passing the actual source directory (not just ".") is essential for the compiler
145+
to calculate correct relative import paths between files in subdirectories.
146+
*)
137147
let module_system_flag = "-bs-module-system " ^ string_of_format format in
138148
let suffix_flag = "-bs-suffix " ^ suffix in
139-
(* For in-source, use "." as base dir; for out-of-source, use lib/es6 etc.
140-
bsc will extract source subdirectory from output_prefix automatically *)
141149
let output_path =
142-
if in_source then "." else Bsb_config.top_prefix_of_format format
150+
if in_source then
151+
(* Pass the actual source directory from $in_d (e.g., "src/core/intl") *)
152+
dir
153+
else
154+
(* Combine base output directory with source subdirectory
155+
to preserve directory structure (e.g., "lib/es6/src/core/intl") *)
156+
Bsb_config.top_prefix_of_format format // dir
143157
in
144158
let output_flag = "-bs-package-output " ^ output_path in
145-
(* Concatenate all three flags *)
146159
module_system_flag ^ " " ^ suffix_flag ^ " " ^ output_flag
147160

148161
(* FIXME: we should adapt it *)

compiler/core/js_name_of_module_id.ml

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,71 @@ let string_of_module_id
148148
Ext_namespace.js_name_of_modulename dep_module_id.id.name case suffix in
149149

150150
if Js_packages_info.same_package_by_name current_package_info dep_package_info then
151-
Ext_path.node_rebase_file
152-
~from:cur_pkg.rel_path
153-
~to_:dep_pkg.rel_path
154-
js_file
151+
(* Same-package imports: both files are in the same package.
152+
153+
Rewatch passes the full directory path via -bs-package-output, e.g.
154+
"/Users/barry/Projects/great-project/src/core/intl", so rel_path contains
155+
the actual directory.
156+
157+
BSB passes ninja's $in_d variable which expands per-file to the source directory.
158+
With the fix in bsb_package_specs.ml, this also contains the full source directory
159+
path, e.g. "/Users/barry/Projects/great-project/src/core/intl".
160+
161+
When both rel_path = ".":
162+
- Current file in src/core/Core_TempTests.res has rel_path = "."
163+
- Dependency in src/core/intl/Core_IntlTests.res has rel_path = "."
164+
165+
Calling node_rebase_file(".", ".", "Core_IntlTests.mjs") would incorrectly
166+
produce "./Core_IntlTests.mjs" when we need "./intl/Core_IntlTests.mjs".
167+
168+
To handle this, we extract the actual source directory from the dependency's
169+
.cmj file path.
170+
*)
171+
if cur_pkg.rel_path = "." && dep_pkg.rel_path = "." then
172+
(* Both rel_path are "." - extract actual source directories from .cmj locations.
173+
174+
In-source builds store .cmj files at lib/bs/<source_dir>/<module>.cmj
175+
Example: /Users/barry/Projects/great-project/lib/bs/src/core/intl/Core_IntlTests.cmj
176+
177+
We extract the source directory to calculate correct relative import paths. *)
178+
let cmj_file = dep_module_id.id.name ^ Literals.suffix_cmj in
179+
match Config_util.find_opt cmj_file with
180+
| Some cmj_path ->
181+
let cmj_dir = Filename.dirname cmj_path in
182+
let lib_bs_pattern = "/lib/bs/" in
183+
let source_dir =
184+
try
185+
(* Find "/lib/bs/" in the path and extract everything after it *)
186+
let idx = String.rindex_from cmj_dir (String.length cmj_dir - 1) '/' in
187+
let rec find_lib_bs pos =
188+
if pos < 0 then None
189+
else if Ext_string.starts_with (String.sub cmj_dir pos (String.length cmj_dir - pos)) lib_bs_pattern then
190+
Some (pos + String.length lib_bs_pattern)
191+
else
192+
find_lib_bs (pos - 1)
193+
in
194+
match find_lib_bs idx with
195+
| Some start_idx ->
196+
(* Example: extract "src/core/intl" from ".../lib/bs/src/core/intl" *)
197+
String.sub cmj_dir start_idx (String.length cmj_dir - start_idx)
198+
| None -> cmj_dir
199+
with Not_found -> cmj_dir
200+
in
201+
Ext_path.node_rebase_file
202+
~from:(Ext_path.absolute_cwd_path output_dir)
203+
~to_:(Ext_path.absolute_cwd_path source_dir)
204+
(Filename.basename js_file)
205+
| None ->
206+
Ext_path.node_rebase_file
207+
~from:cur_pkg.rel_path
208+
~to_:dep_pkg.rel_path
209+
js_file
210+
else
211+
(* rel_path values contain directory information, use them directly *)
212+
Ext_path.node_rebase_file
213+
~from:cur_pkg.rel_path
214+
~to_:dep_pkg.rel_path
215+
js_file
155216
(* TODO: we assume that both [x] and [path] could only be relative path
156217
which is guaranteed by [-bs-package-output]
157218
*)
@@ -161,7 +222,56 @@ let string_of_module_id
161222
else
162223
begin match module_system with
163224
| Commonjs | Esmodule ->
164-
dep_pkg.pkg_rel_path // js_file
225+
(* External package imports: importing from a different package.
226+
227+
When dep_pkg.rel_path = "." (dependency uses in-source builds),
228+
pkg_rel_path becomes "package_name/." (e.g., "a/."), which would
229+
generate invalid imports like "a/./A.res.js" instead of "a/src/A.res.js".
230+
231+
We extract the actual source directory from the dependency's .cmj file
232+
location and reconstruct the import path correctly.
233+
*)
234+
if dep_pkg.rel_path = "." then
235+
let cmj_file = dep_module_id.id.name ^ Literals.suffix_cmj in
236+
match Config_util.find_opt cmj_file with
237+
| Some cmj_path ->
238+
(* External packages store .cmj at node_modules/<pkg>/lib/bs/<source_dir>/<module>.cmj
239+
Example: /Users/barry/Projects/great-project/node_modules/a/lib/bs/src/A-A.cmj
240+
We extract "src" from this path. *)
241+
let cmj_dir = Filename.dirname cmj_path in
242+
let lib_bs_pattern = "/lib/bs/" in
243+
let source_dir =
244+
try
245+
let idx = String.rindex_from cmj_dir (String.length cmj_dir - 1) '/' in
246+
let rec find_lib_bs pos =
247+
if pos < 0 then None
248+
else if Ext_string.starts_with (String.sub cmj_dir pos (String.length cmj_dir - pos)) lib_bs_pattern then
249+
Some (pos + String.length lib_bs_pattern)
250+
else
251+
find_lib_bs (pos - 1)
252+
in
253+
match find_lib_bs idx with
254+
| Some start_idx ->
255+
String.sub cmj_dir start_idx (String.length cmj_dir - start_idx)
256+
| None -> "."
257+
with Not_found -> "."
258+
in
259+
(* Extract package name from pkg_rel_path: "a/." -> "a" *)
260+
let pkg_name =
261+
try
262+
let idx = String.rindex dep_pkg.pkg_rel_path '/' in
263+
String.sub dep_pkg.pkg_rel_path 0 idx
264+
with Not_found -> dep_pkg.pkg_rel_path
265+
in
266+
if source_dir = "." then
267+
dep_pkg.pkg_rel_path // js_file
268+
else
269+
(* Reconstruct: "a" + "src" + "A.res.js" = "a/src/A.res.js" *)
270+
pkg_name // source_dir // js_file
271+
| None ->
272+
dep_pkg.pkg_rel_path // js_file
273+
else
274+
dep_pkg.pkg_rel_path // js_file
165275
(* Note we did a post-processing when working on Windows *)
166276
| Es6_global
167277
->

tests/tests/src/core/Core_TempTests.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
77
import * as Stdlib_Float from "@rescript/runtime/lib/es6/Stdlib_Float.js";
88
import * as Stdlib_BigInt from "@rescript/runtime/lib/es6/Stdlib_BigInt.js";
99
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10-
import * as Core_IntlTests from "./Core_IntlTests.mjs";
10+
import * as Core_IntlTests from "./intl/Core_IntlTests.mjs";
1111
import * as Primitive_bigint from "@rescript/runtime/lib/es6/Primitive_bigint.js";
1212
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
1313

0 commit comments

Comments
 (0)