@@ -24,15 +24,119 @@ use wasm_bindgen::prelude::*;
2424#[ cfg_attr( target_arch = "wasm32" , wasm_bindgen) ]
2525#[ cfg_attr( target_arch = "wasm32" , no_mangle) ]
2626pub fn convert_lockfile_to_nix_expression ( contents : String , options : Options ) -> Result < String > {
27+ let packages = build_packages ( & contents) ?;
28+
29+ NixExpression :: new ( packages) ?. render_with_options ( options)
30+ }
31+
32+ /// # Build Packages from a Lockfile
33+ ///
34+ /// Parses a bun lockfile and produces the sorted, de-duplicated list of
35+ /// [`Package`]s it describes.
36+ pub fn build_packages ( contents : & str ) -> Result < Vec < Package > > {
2737 let lockfile = contents. parse :: < Lockfile > ( ) ?;
2838
2939 if lockfile. lockfile_version != 1 {
3040 return Err ( Error :: UnsupportedLockfileVersion ( lockfile. lockfile_version ) ) ;
3141 } ;
3242
43+ // Workspace name → directory, for resolving nested file-dependency paths:
44+ // bun records a `<workspace-name>/<pkg>` entry's path relative to that
45+ // workspace's directory, but `bun.nix` paths resolve from the project root.
46+ let workspace_dirs: Vec < ( String , String ) > = lockfile
47+ . workspaces
48+ . iter ( )
49+ . filter_map ( |( dir, ws) | ws. name . clone ( ) . map ( |name| ( name, dir. clone ( ) ) ) )
50+ . collect ( ) ;
51+
3352 let mut packages = lockfile. packages ( ) ;
3453 packages. sort ( ) ;
3554 packages. dedup_by ( |a, b| a. name == b. name ) ;
3655
37- NixExpression :: new ( packages) ?. render_with_options ( options)
56+ for package in & mut packages {
57+ if let package:: Fetcher :: CopyToStore { path } = & mut package. fetcher {
58+ // Longest matching `<workspace-name>/` prefix wins; entries whose
59+ // key IS a workspace name (the workspaces themselves) don't match.
60+ let parent_dir = workspace_dirs
61+ . iter ( )
62+ . filter ( |( name, _) | {
63+ !name. is_empty ( )
64+ && package. name . len ( ) > name. len ( ) + 1
65+ && package. name . starts_with ( name. as_str ( ) )
66+ && package. name . as_bytes ( ) [ name. len ( ) ] == b'/'
67+ } )
68+ . max_by_key ( |( name, _) | name. len ( ) )
69+ . map ( |( _, dir) | dir. as_str ( ) ) ;
70+ if let Some ( dir) = parent_dir
71+ && !dir. is_empty ( )
72+ {
73+ * path = normalize_path ( & format ! ( "{dir}/{path}" ) ) ;
74+ }
75+ }
76+ }
77+
78+ Ok ( packages)
79+ }
80+
81+ /// Collapse `.` and `..` segments lexically (`a/b/../c` → `a/c`).
82+ fn normalize_path ( path : & str ) -> String {
83+ let mut parts: Vec < & str > = Vec :: new ( ) ;
84+ for seg in path. split ( '/' ) {
85+ match seg {
86+ "" | "." => { }
87+ ".." => {
88+ if parts. last ( ) . is_none_or ( |last| * last == ".." ) {
89+ parts. push ( ".." ) ;
90+ } else {
91+ parts. pop ( ) ;
92+ }
93+ }
94+ s => parts. push ( s) ,
95+ }
96+ }
97+ parts. join ( "/" )
98+ }
99+
100+ #[ cfg( test) ]
101+ mod tests {
102+ use super :: * ;
103+
104+ // A vendored tarball nested under a workspace ("<ws-name>/<pkg>") is
105+ // recorded relative to the workspace dir; bun.nix needs it root-relative.
106+ #[ test]
107+ fn nested_file_dep_paths_resolve_against_workspace_dir ( ) {
108+ let lock = r#"{
109+ "lockfileVersion": 1,
110+ "workspaces": {
111+ "": { "name": "root" },
112+ "packages/app": { "name": "@oc/app" },
113+ "packages/ui": { "name": "@oc/ui" }
114+ },
115+ "packages": {
116+ "@oc/app": ["@oc/app@workspace:packages/app"],
117+ "@oc/ui": ["@oc/ui@workspace:packages/ui"],
118+ "@oc/app/@oc/client": ["@oc/client@vendor/client-1.0.0.tgz", {}, "sha512-AAAA"],
119+ "@oc/ui/@oc/client": ["@oc/client@../app/vendor/client-1.0.0.tgz", {}, "sha512-AAAA"],
120+ }
121+ }"# ;
122+ let pkgs = build_packages ( lock) . unwrap ( ) ;
123+ let path_of = |name : & str | {
124+ let p = pkgs. iter ( ) . find ( |p| p. name == name) . unwrap ( ) ;
125+ match & p. fetcher {
126+ package:: Fetcher :: CopyToStore { path } => path. clone ( ) ,
127+ other => panic ! ( "expected CopyToStore, got {other:?}" ) ,
128+ }
129+ } ;
130+
131+ assert_eq ! (
132+ path_of( "@oc/app/@oc/client" ) ,
133+ "packages/app/vendor/client-1.0.0.tgz"
134+ ) ;
135+ assert_eq ! (
136+ path_of( "@oc/ui/@oc/client" ) ,
137+ "packages/app/vendor/client-1.0.0.tgz"
138+ ) ;
139+ // Workspace entries themselves stay untouched.
140+ assert_eq ! ( path_of( "@oc/app" ) , "packages/app" ) ;
141+ }
38142}
0 commit comments