Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add module_alias resolution capabilities #7185

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/commands/commandUtils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ let file_options =
module_file_exts = FlowConfig.module_file_exts flowconfig;
module_resource_exts = FlowConfig.module_resource_exts flowconfig;
node_resolver_dirnames = FlowConfig.node_resolver_dirnames flowconfig;
node_resolver_aliases = FlowConfig.node_resolver_aliases flowconfig
}

let ignore_flag prev = CommandSpec.ArgSpec.(
Expand Down
12 changes: 12 additions & 0 deletions src/commands/config/flowConfig.ml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ module Opts = struct
munge_underscores: bool;
no_flowlib: bool;
node_resolver_dirnames: string list;
node_resolver_aliases: string list;
root_name: string option;
saved_state_fetcher: Options.saved_state_fetcher;
shm_dep_table_pow: int;
Expand Down Expand Up @@ -155,6 +156,7 @@ module Opts = struct
munge_underscores = false;
no_flowlib = false;
node_resolver_dirnames = ["node_modules"];
node_resolver_aliases = [];
root_name = None;
saved_state_fetcher = Options.Dummy_fetcher;
shm_dep_table_pow = 17;
Expand Down Expand Up @@ -486,6 +488,15 @@ module Opts = struct
let node_resolver_dirnames = v :: opts.node_resolver_dirnames in
Ok {opts with node_resolver_dirnames;}
);

"module.system.node.resolve_alias",
string
~init: (fun opts -> { opts with node_resolver_aliases = [] })
~multiple: true
(fun opts v ->
let node_resolver_aliases = v :: opts.node_resolver_aliases in
Ok {opts with node_resolver_aliases;}
);

"module.use_strict",
boolean (fun opts v -> Ok { opts with modules_are_use_strict = v });
Expand Down Expand Up @@ -991,6 +1002,7 @@ let modules_are_use_strict c = c.options.Opts.modules_are_use_strict
let munge_underscores c = c.options.Opts.munge_underscores
let no_flowlib c = c.options.Opts.no_flowlib
let node_resolver_dirnames c = c.options.Opts.node_resolver_dirnames
let node_resolver_aliases c = c.options.Opts.node_resolver_aliases
let root_name c = c.options.Opts.root_name
let saved_state_fetcher c = c.options.Opts.saved_state_fetcher
let shm_dep_table_pow c = c.options.Opts.shm_dep_table_pow
Expand Down
1 change: 1 addition & 0 deletions src/commands/config/flowConfig.mli
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ val modules_are_use_strict: config -> bool
val munge_underscores: config -> bool
val no_flowlib: config -> bool
val node_resolver_dirnames: config -> string list
val node_resolver_aliases: config -> string list
val required_version: config -> string option
val root_name: config -> string option
val saved_state_fetcher: config -> Options.saved_state_fetcher
Expand Down
27 changes: 22 additions & 5 deletions src/common/files.ml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type options = {
module_file_exts: SSet.t;
module_resource_exts: SSet.t;
node_resolver_dirnames: string list;
node_resolver_aliases: string list;
}

let default_lib_dir options = options.default_lib_dir
Expand All @@ -28,6 +29,7 @@ let lib_paths options = options.lib_paths
let module_file_exts options = options.module_file_exts
let module_resource_exts options = options.module_resource_exts
let node_resolver_dirnames options = options.node_resolver_dirnames
let node_resolver_aliases options = options.node_resolver_aliases

let node_modules_containers = ref SSet.empty

Expand Down Expand Up @@ -104,6 +106,9 @@ let is_valid_path =
let is_node_module options path =
List.mem (Filename.basename path) options.node_resolver_dirnames

let is_module_alias options path =
List.mem (Filename.basename path) options.node_resolver_aliases

let is_flow_file ~options =
let is_valid_path = is_valid_path ~options in
fun path -> is_valid_path path && not (is_directory path)
Expand Down Expand Up @@ -177,7 +182,7 @@ let max_files = 1000

If kind_of_path fails, then we only emit a warning if error_filter passes *)
let make_next_files_and_symlinks
~node_module_filter ~path_filter ~realpath_filter ~error_filter paths =
~node_module_filter ~module_alias_filter ~path_filter ~realpath_filter ~error_filter paths =
let prefix_checkers = Core_list.map ~f:is_prefix paths in
let rec process sz (acc, symlinks) files dir stack =
if sz >= max_files then
Expand All @@ -193,7 +198,7 @@ let make_next_files_and_symlinks
then process (sz+1) (real :: acc, symlinks) files dir stack
else process sz (acc, symlinks) files dir stack
| Dir (path, is_symlink) ->
if node_module_filter file
if node_module_filter file || module_alias_filter file
then node_modules_containers := SSet.add (Filename.dirname file) !node_modules_containers;
let dirfiles = Array.to_list @@ try_readdir path in
let symlinks =
Expand Down Expand Up @@ -229,13 +234,14 @@ let make_next_files_and_symlinks
of `paths`. *)
let make_next_files_following_symlinks
~node_module_filter
~module_alias_filter
~path_filter
~realpath_filter
~error_filter
paths =
let paths = Core_list.map ~f:Path.to_string paths in
let cb = ref (make_next_files_and_symlinks
~node_module_filter ~path_filter ~realpath_filter ~error_filter paths
~node_module_filter ~module_alias_filter ~path_filter ~realpath_filter ~error_filter paths
) in
let symlinks = ref SSet.empty in
let seen_symlinks = ref SSet.empty in
Expand All @@ -254,7 +260,7 @@ let make_next_files_following_symlinks
symlinks := SSet.empty;
(* since we're following a symlink, use realpath_filter for both *)
cb := make_next_files_and_symlinks
~node_module_filter ~path_filter:realpath_filter ~realpath_filter ~error_filter paths;
~node_module_filter ~module_alias_filter ~path_filter:realpath_filter ~realpath_filter ~error_filter paths;
rec_cb ()
end
in
Expand All @@ -273,6 +279,7 @@ let get_all =

let init ?(flowlibs_only=false) (options: options) =
let node_module_filter = is_node_module options in
let module_alias_filter = is_module_alias options in
let libs = if flowlibs_only then [] else options.lib_paths in
let libs, filter = match options.default_lib_dir with
| None -> libs, is_valid_path ~options
Expand All @@ -291,6 +298,7 @@ let init ?(flowlibs_only=false) (options: options) =
let filter' path = path = lib_str || filter path in
make_next_files_following_symlinks
~node_module_filter
~module_alias_filter
~path_filter:filter'
~realpath_filter:filter'
~error_filter:(fun _ -> true)
Expand Down Expand Up @@ -364,6 +372,7 @@ let watched_paths options =
*)
let make_next_files ~root ~all ~subdir ~options ~libs =
let node_module_filter = is_node_module options in
let module_alias_filter = is_module_alias options in
let filter = if all then fun _ -> true else wanted ~options libs in

(* The directories from which we start our search *)
Expand Down Expand Up @@ -399,7 +408,7 @@ let make_next_files ~root ~all ~subdir ~options ~libs =
)
in
make_next_files_following_symlinks
~node_module_filter ~path_filter ~realpath_filter ~error_filter:filter starting_points
~node_module_filter ~module_alias_filter ~path_filter ~realpath_filter ~error_filter:filter starting_points

let is_windows_root root =
Sys.win32 &&
Expand Down Expand Up @@ -527,6 +536,14 @@ let is_within_node_modules ~root ~options path =
let node_resolver_dirnames = node_resolver_dirnames options |> SSet.of_list in
not (SSet.inter directories node_resolver_dirnames |> SSet.is_empty)

(* Given a path, we want to know if it's a resolve alias. *)
let is_within_alias_directory ~root ~options path =
(* We use paths that are relative to the root, so that we ignore ancestor directories *)
let path = relative_path (Path.to_string root) path in
let directories = Str.split dir_sep path |> SSet.of_list in
let node_resolver_aliases = node_resolver_aliases options |> SSet.of_list in
not (SSet.inter directories node_resolver_aliases |> SSet.is_empty)

(* realpath doesn't work for non-existent paths. So let's find the longest existent prefix, run
* realpath on that, and then append the rest to it
*)
Expand Down
3 changes: 3 additions & 0 deletions src/common/files.mli
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type options = {
module_file_exts: SSet.t;
module_resource_exts: SSet.t;
node_resolver_dirnames: string list;
node_resolver_aliases: string list;
}

val default_lib_dir: options -> Path.t option
Expand All @@ -28,6 +29,7 @@ val lib_paths: options -> Path.t list
val module_file_exts: options -> SSet.t
val module_resource_exts: options -> SSet.t
val node_resolver_dirnames: options -> string list
val node_resolver_aliases: options -> string list

val node_modules_containers: SSet.t ref

Expand Down Expand Up @@ -106,6 +108,7 @@ val filename_from_string: options: options -> string -> File_key.t
val mkdirp: string -> Unix.file_perm -> unit

val is_within_node_modules: root:Path.t -> options: options -> string -> bool
val is_within_alias_directory: root:Path.t -> options: options -> string -> bool

val imaginary_realpath: string -> string
val canonicalize_filenames:
Expand Down
24 changes: 18 additions & 6 deletions src/services/inference/module/module_js.ml
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,24 @@ module Node = struct
lazy_seq [
lazy (
if SSet.mem dir node_modules_containers then
lazy_seq (Files.node_resolver_dirnames file_options |> Core_list.map ~f:(fun dirname ->
lazy (resolve_relative
~options ~reader
loc ?resolution_acc dir (spf "%s%s%s" dirname Filename.dir_sep r)
)
))
lazy_seq([
lazy (
lazy_seq (Files.node_resolver_aliases file_options |> Core_list.map ~f:(fun dirname ->
lazy (resolve_relative
~options ~reader
loc ?resolution_acc dir (spf "%s%s%s" dirname Filename.dir_sep r)
)
))
);
lazy (
lazy_seq (Files.node_resolver_dirnames file_options |> Core_list.map ~f:(fun dirname ->
lazy (resolve_relative
~options ~reader
loc ?resolution_acc dir (spf "%s%s%s" dirname Filename.dir_sep r)
)
))
);
])
else None
);

Expand Down
7 changes: 7 additions & 0 deletions tests/config_module_system_node_resolve_alias/.flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[options]
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=custom_node_modules
module.system.node.resolve_alias=custom_resolve_dir
; should also be able to specify as
; <PROJECT_ROOT>/root_resolve_dir
module.system.node.resolve_alias=./root_resolve_dir
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Error ------------------------------------------------------------------------------------------ subdir/sublevel.js:12:2

Cannot cast `two` to string literal `custom_resolve_dir/testproj2` because string literal
`subdir/custom_resolve_dir/testproj2` [1] is incompatible with string literal `custom_resolve_dir/testproj2` [2].

subdir/sublevel.js:12:2
12| (two: "custom_resolve_dir/testproj2"); // Error: Resolve from sibling 'custom_resolve_dir' first!
^^^

References:
subdir/custom_resolve_dir/testproj2/index.js:3:18
3| export var name: "subdir/custom_resolve_dir/testproj2" = "subdir/custom_resolve_dir/testproj2";
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [1]
subdir/sublevel.js:12:7
12| (two: "custom_resolve_dir/testproj2"); // Error: Resolve from sibling 'custom_resolve_dir' first!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]


Error ------------------------------------------------------------------------------------------ subdir/sublevel.js:13:2

Cannot cast `two` to string literal `node_modules/testproj2` because string literal
`subdir/custom_resolve_dir/testproj2` [1] is incompatible with string literal `node_modules/testproj2` [2].

subdir/sublevel.js:13:2
13| (two: "node_modules/testproj2"); // Error: Resolve from sibling 'custom_resolve_dir' first!
^^^

References:
subdir/custom_resolve_dir/testproj2/index.js:3:18
3| export var name: "subdir/custom_resolve_dir/testproj2" = "subdir/custom_resolve_dir/testproj2";
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [1]
subdir/sublevel.js:13:7
13| (two: "node_modules/testproj2"); // Error: Resolve from sibling 'custom_resolve_dir' first!
^^^^^^^^^^^^^^^^^^^^^^^^ [2]


Error -------------------------------------------------------------------------------------------------- toplevel.js:7:2

Cannot cast `one` to string literal `node_modules/testproj` because string literal `custom_resolve_dir/testproj` [1] is
incompatible with string literal `node_modules/testproj` [2].

toplevel.js:7:2
7| (one: "node_modules/testproj"); // Error: Resolve from resolve_alias first!
^^^

References:
custom_resolve_dir/testproj/index.js:3:18
3| export var name: "custom_resolve_dir/testproj" = "custom_resolve_dir/testproj";
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [1]
toplevel.js:7:7
7| (one: "node_modules/testproj"); // Error: Resolve from resolve_alias first!
^^^^^^^^^^^^^^^^^^^^^^^ [2]



Found 3 errors

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export var name: "custom_resolve_dir/testproj" = "custom_resolve_dir/testproj";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export var name: "custom_resolve_dir/testproj2" = "custom_resolve_dir/testproj2";
8 changes: 8 additions & 0 deletions tests/config_module_system_node_resolve_alias/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": ["./src/*"]
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export var name: "root_resolve_dir/testproj6" = "root_resolve_dir/testproj6";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export var name: "subdir/custom_resolve_dir/testproj2" = "subdir/custom_resolve_dir/testproj2";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @flow

export var name: "subdir/custom_resolve_dir/testproj5" =
"subdir/custom_resolve_dir/testproj5";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @flow

export var name: "subdir/root_resolve_dir/testproj6" =
"subdir/root_resolve_dir/testproj6";
39 changes: 39 additions & 0 deletions tests/config_module_system_node_resolve_alias/subdir/sublevel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @flow
import { name as one } from "testproj";
import { name as two } from "testproj2";
import { name as three } from "testproj2/subfile";

import { name as four } from "testproj3";
import { name as five } from "testproj4";

import { name as six } from "testproj5";

import { name as seven } from "testproj6";

(one: "custom_resolve_dir/testproj");

(two: "subdir/custom_resolve_dir/testproj2");
(two: "custom_resolve_dir/testproj2"); // Error: Resolve from sibling 'custom_resolve_dir' first!
(two: "node_modules/testproj2"); // Error: Resolve from sibling 'custom_resolve_dir' first!

// if we fail to resolve in custom_resolve_dir
// then we should still resolve node_modules if
// possible.
//
// this tends to bring up the possibility of
// confusing bugs but it is consistent with
// the behavior of the webpack/babel features
// they are based upon.
//
// personally can see a flow lint warning
// as an option here if a conflict of folders
// is detected between aliases and/or node_modules
(three: "node_modules/testproj2/subfile");

// should still resolve node_modules
(four: "node_modules/testproj3");

// should still resolve custom_node_modules
(five: "custom_node_modules/testproj4");

(seven: "root_resolve_dir/testproj6/");
Loading