Skip to content

Commit e22f5bb

Browse files
committed
Fix JavaScript import paths for in-source builds after splitting -bs-package-output
For in-source builds, extract source subdirectories from output_prefix and .cmj file locations to generate correct relative import paths.
1 parent 957acbf commit e22f5bb

File tree

3 files changed

+172
-76
lines changed

3 files changed

+172
-76
lines changed

compiler/bsb/bsb_package_specs.ml

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,28 +132,18 @@ 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 =
135+
let package_flag ({format; in_source; suffix} : Bsb_spec_set.spec) _dir =
136136
(* Generates three separate compiler flags that were split from the original
137137
single "-bs-package-output module:path:suffix" flag.
138138
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.
139+
For in-source builds, pass "." as the base directory. The compiler will
140+
extract the source subdirectory from output_prefix to construct the correct path.
141+
For out-of-source builds, pass the lib output directory (e.g., "lib/es6").
146142
*)
147143
let module_system_flag = "-bs-module-system " ^ string_of_format format in
148144
let suffix_flag = "-bs-suffix " ^ suffix in
149145
let output_path =
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
146+
if in_source then "." else Bsb_config.top_prefix_of_format format
157147
in
158148
let output_flag = "-bs-package-output " ^ output_path in
159149
module_system_flag ^ " " ^ suffix_flag ^ " " ^ output_flag
@@ -197,7 +187,15 @@ let get_list_of_output_js (package_specs : t)
197187
Ext_namespace.change_ext_ns_suffix output_file_sans_extension
198188
spec.suffix
199189
in
200-
(if spec.in_source then Bsb_config.rev_lib_bs_prefix basename
190+
(if spec.in_source then
191+
(* When dir=".", name_sans_extension can be "./Module" (from Filename.concat "." "Module").
192+
Strip the "./" prefix to avoid generating "../.././Module.res.js" *)
193+
let basename_clean =
194+
if Ext_string.starts_with basename "./" then
195+
String.sub basename 2 (String.length basename - 2)
196+
else basename
197+
in
198+
Bsb_config.rev_lib_bs_prefix basename_clean
201199
else Bsb_config.lib_bs_prefix_of_format spec.format // basename)
202200
:: acc)
203201
package_specs.modules []

compiler/core/js_name_of_module_id.ml

Lines changed: 148 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ let string_of_module_id
129129

130130
let dep_info_query =
131131
Js_packages_info.query_package_infos dep_package_info module_system
132-
in
132+
in
133133
match dep_info_query, current_info_query with
134134
| Package_not_found , _ ->
135135
Bs_exception.error (Missing_ml_dependency dep_module_id.id.name)
@@ -138,10 +138,51 @@ let string_of_module_id
138138
| (Package_script | Package_found _ ), Package_not_found -> assert false
139139

140140
| Package_found ({suffix} as pkg), Package_script
141-
->
141+
->
142142
let js_file =
143-
Ext_namespace.js_name_of_modulename dep_module_id.id.name case suffix in
144-
pkg.pkg_rel_path // js_file
143+
Ext_namespace.js_name_of_modulename dep_module_id.id.name case suffix in
144+
(* External package imports: check if pkg_rel_path ends with "/."
145+
which indicates the dependency uses in-source builds *)
146+
if Ext_string.ends_with pkg.pkg_rel_path "/." then begin
147+
let cmj_file = dep_module_id.id.name ^ Literals.suffix_cmj in
148+
match Config_util.find_opt cmj_file with
149+
| Some cmj_path ->
150+
(* External packages store .cmj at node_modules/<pkg>/lib/bs/<source_dir>/<module>.cmj
151+
Example: /Users/barry/Projects/great-project/node_modules/a/lib/bs/src/A-A.cmj
152+
We extract "src" from this path. *)
153+
let cmj_dir = Filename.dirname cmj_path in
154+
let lib_bs_pattern = "/lib/bs/" in
155+
let source_dir =
156+
try
157+
let rec find_lib_bs pos =
158+
if pos < 0 then None
159+
else if Ext_string.starts_with (String.sub cmj_dir pos (String.length cmj_dir - pos)) lib_bs_pattern then
160+
Some (pos + String.length lib_bs_pattern)
161+
else
162+
find_lib_bs (pos - 1)
163+
in
164+
match find_lib_bs (String.length cmj_dir - 1) with
165+
| Some start_idx ->
166+
String.sub cmj_dir start_idx (String.length cmj_dir - start_idx)
167+
| None -> "."
168+
with Not_found -> "."
169+
in
170+
(* Extract package name from pkg_rel_path: "a/." -> "a" *)
171+
let pkg_name =
172+
String.sub pkg.pkg_rel_path 0 (String.length pkg.pkg_rel_path - 2)
173+
in
174+
if source_dir = "." then begin
175+
pkg.pkg_rel_path // js_file
176+
end else begin
177+
let result = pkg_name // source_dir // js_file in
178+
(* Reconstruct: "a" + "src" + "A.res.js" = "a/src/A.res.js" *)
179+
result
180+
end
181+
| None ->
182+
pkg.pkg_rel_path // js_file
183+
end else begin
184+
pkg.pkg_rel_path // js_file
185+
end
145186
| Package_found ({suffix } as dep_pkg),
146187
Package_found cur_pkg ->
147188
let js_file =
@@ -216,62 +257,113 @@ let string_of_module_id
216257
(* TODO: we assume that both [x] and [path] could only be relative path
217258
which is guaranteed by [-bs-package-output]
218259
*)
219-
else
220-
if Js_packages_info.is_runtime_package dep_package_info then
221-
get_runtime_module_path dep_module_id current_package_info module_system
222-
else
223-
begin match module_system with
224-
| Commonjs | Esmodule ->
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)
260+
else
261+
if Js_packages_info.is_runtime_package dep_package_info then
262+
get_runtime_module_path dep_module_id current_package_info module_system
263+
else begin
264+
match module_system with
265+
| Commonjs | Esmodule ->
266+
(* External package imports: importing from a different package.
267+
268+
When dep_pkg.rel_path = "." (dependency uses in-source builds),
269+
pkg_rel_path becomes "package_name/." (e.g., "a/."), which would
270+
generate invalid imports like "a/./A.res.js" instead of "a/src/A.res.js".
271+
272+
We extract the actual source directory from the dependency's .cmj file
273+
location and reconstruct the import path correctly.
274+
*)
275+
(* External package imports: check if pkg_rel_path ends with "/."
276+
which indicates the dependency uses in-source builds *)
277+
if Ext_string.ends_with dep_pkg.pkg_rel_path "/." then begin
278+
let cmj_file = dep_module_id.id.name ^ Literals.suffix_cmj in
279+
(* Prefer lib/bs over lib/ocaml as lib/bs preserves source directory structure *)
280+
let cmj_opt =
281+
match Config_util.find_opt cmj_file with
282+
| Some path when Ext_string.contain_substring path "/lib/bs/" ->
283+
Some path
284+
| Some ocaml_path ->
285+
(* Found lib/ocaml, derive lib/bs path from it *)
286+
let lib_ocaml_pattern = "/lib/ocaml/" in
287+
let pkg_root =
288+
try
289+
let rec find_lib_ocaml pos =
290+
if pos < 0 then None
291+
else if Ext_string.starts_with (String.sub ocaml_path pos (String.length ocaml_path - pos)) lib_ocaml_pattern then
292+
Some (String.sub ocaml_path 0 pos)
293+
else
294+
find_lib_ocaml (pos - 1)
295+
in
296+
find_lib_ocaml (String.length ocaml_path - 1)
297+
with Not_found -> None
252298
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 -> "."
299+
(match pkg_root with
300+
| Some root ->
301+
(* The actual cmj file is in lib/bs/src/, not lib/bs/ directly
302+
Try a glob search to find it *)
303+
let rec find_in_dir dir =
304+
let full_path = Filename.concat dir cmj_file in
305+
if Sys.file_exists full_path then Some full_path
306+
else
307+
try
308+
let subdirs = Sys.readdir dir in
309+
Array.fold_left (fun acc subdir ->
310+
match acc with
311+
| Some _ -> acc
312+
| None ->
313+
let sub_path = Filename.concat dir subdir in
314+
if Sys.is_directory sub_path then find_in_dir sub_path
315+
else None
316+
) None subdirs
317+
with _ -> None
318+
in
319+
let lib_bs_dir = root ^ "/lib/bs" in
320+
(match find_in_dir lib_bs_dir with
321+
| Some bs_path ->
322+
Some bs_path
323+
| None ->
324+
Some ocaml_path)
325+
| None -> Some ocaml_path)
326+
| None -> None
258327
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
328+
match cmj_opt with
329+
| Some cmj_path ->
330+
(* External packages store .cmj at node_modules/<pkg>/lib/bs/<source_dir>/<module>.cmj
331+
Example: /Users/barry/Projects/rescript/node_modules/a/lib/bs/src/A-A.cmj
332+
We extract "src" from this path. *)
333+
let cmj_dir = Filename.dirname cmj_path in
334+
let lib_bs_pattern = "/lib/bs/" in
335+
let source_dir =
336+
try
337+
let idx = String.rindex_from cmj_dir (String.length cmj_dir - 1) '/' in
338+
let rec find_lib_bs pos =
339+
if pos < 0 then None
340+
else if Ext_string.starts_with (String.sub cmj_dir pos (String.length cmj_dir - pos)) lib_bs_pattern then
341+
Some (pos + String.length lib_bs_pattern)
342+
else
343+
find_lib_bs (pos - 1)
344+
in
345+
match find_lib_bs idx with
346+
| Some start_idx ->
347+
String.sub cmj_dir start_idx (String.length cmj_dir - start_idx)
348+
| None -> "."
349+
with Not_found -> "."
350+
in
351+
(* Extract package name from pkg_rel_path: "a/." -> "a" *)
352+
let pkg_name =
353+
String.sub dep_pkg.pkg_rel_path 0 (String.length dep_pkg.pkg_rel_path - 2)
354+
in
355+
if source_dir = "." then begin
356+
dep_pkg.pkg_rel_path // js_file
357+
end else begin
358+
let result = pkg_name // source_dir // js_file in
359+
(* Reconstruct: "a" + "src" + "A.res.js" = "a/src/A.res.js" *)
360+
result
361+
end
362+
| None ->
267363
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 ->
364+
end else begin
272365
dep_pkg.pkg_rel_path // js_file
273-
else
274-
dep_pkg.pkg_rel_path // js_file
366+
end
275367
(* Note we did a post-processing when working on Windows *)
276368
| Es6_global
277369
->

compiler/core/lam_compile_main.ml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,16 @@ let lambda_as_module
312312
if path = "." || path = "lib/bs" || path = "lib/es6" || path = "lib/es6-global" then
313313
(* Legacy bsb mode: path is base dir, extract source subdir from output_prefix *)
314314
let source_subdir = Filename.dirname output_prefix in
315-
(Lazy.force Ext_path.package_dir //
316-
path //
317-
source_subdir //
318-
basename)
315+
(* When source_subdir is ".", don't include it in the path to avoid "././" *)
316+
if source_subdir = "." then
317+
(Lazy.force Ext_path.package_dir //
318+
path //
319+
basename)
320+
else
321+
(Lazy.force Ext_path.package_dir //
322+
path //
323+
source_subdir //
324+
basename)
319325
else
320326
(* Rewatch mode: path already contains full directory *)
321327
(Lazy.force Ext_path.package_dir //

0 commit comments

Comments
 (0)