src/rally@dependency_resolver.erl

-module(rally@dependency_resolver).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rally/dependency_resolver.gleam").
-export([resolve/3]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Client dependency resolver.\n"
    "\n"
    " Starting from the tree-shaken page modules and layout files, follows\n"
    " their import chains through the server's src/ tree and copies any\n"
    " shared modules the client package needs. Catches @external(erlang)\n"
    " imports that would fail to compile for JavaScript and reports the\n"
    " import chain so the developer can find the problem.\n"
).

-file("src/rally/dependency_resolver.gleam", 106).
-spec extract_imports(binary()) -> list(binary()).
extract_imports(Source) ->
    case glance:module(Source) of
        {ok, Ast} ->
            gleam@list:map(
                erlang:element(2, Ast),
                fun(Def) -> erlang:element(3, erlang:element(3, Def)) end
            );

        _ ->
            []
    end.

-file("src/rally/dependency_resolver.gleam", 113).
-spec collect_ffi_files(binary(), binary(), binary()) -> list(rally@generator@client:generated_file()).
collect_ffi_files(Src_root, Client_root, Module_path) ->
    Ffi_path = <<<<<<Src_root/binary, "/"/utf8>>/binary, Module_path/binary>>/binary,
        "_ffi.mjs"/utf8>>,
    case simplifile:read(Ffi_path) of
        {ok, Content} ->
            Dest = <<<<<<Client_root/binary, "/src/"/utf8>>/binary,
                    Module_path/binary>>/binary,
                "_ffi.mjs"/utf8>>,
            [{generated_file, Dest, Content}];

        _ ->
            []
    end.

-file("src/rally/dependency_resolver.gleam", 170).
-spec do_find_line(list(binary()), binary(), integer()) -> integer().
do_find_line(Lines, Needle, N) ->
    case Lines of
        [] ->
            N;

        [Line | Rest] ->
            case gleam_stdlib:contains_string(Line, Needle) of
                true ->
                    N;

                _ ->
                    do_find_line(Rest, Needle, N + 1)
            end
    end.

-file("src/rally/dependency_resolver.gleam", 164).
-spec find_line_number(binary(), binary()) -> integer().
find_line_number(Content, Needle) ->
    _pipe = Content,
    _pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
    do_find_line(_pipe@1, Needle, 1).

-file("src/rally/dependency_resolver.gleam", 133).
-spec check_erlang_external(binary(), binary(), list(binary())) -> {ok, nil} |
    {error, binary()}.
check_erlang_external(Content, Module_path, Chain) ->
    Has_erlang = gleam_stdlib:contains_string(
        Content,
        <<"@external(erlang,"/utf8>>
    ),
    Has_javascript = gleam_stdlib:contains_string(
        Content,
        <<"@external(javascript,"/utf8>>
    ),
    Error_msg = begin
        Line = find_line_number(Content, <<"@external(erlang,"/utf8>>),
        Chain_str = gleam@string:join(
            gleam@list:map(
                lists:append(Chain, [Module_path]),
                fun(C) -> <<C/binary, ".gleam"/utf8>> end
            ),
            <<" -> "/utf8>>
        ),
        <<<<<<<<<<<<<<<<Module_path/binary, ".gleam (line "/utf8>>/binary,
                                    (erlang:integer_to_binary(Line))/binary>>/binary,
                                ") contains @external(erlang, ...) which can't compile for JavaScript.\n\n"/utf8>>/binary,
                            "  Import chain: "/utf8>>/binary,
                        Chain_str/binary>>/binary,
                    "\n\n"/utf8>>/binary,
                "  Server-only code belongs in page modules as server_* functions (which rally\n"/utf8>>/binary,
            "  strips from the client), or in separate modules that client code doesn't import."/utf8>>
    end,
    gleam@bool:guard(
        Has_erlang andalso not Has_javascript,
        {error, Error_msg},
        fun() -> {ok, nil} end
    ).

-file("src/rally/dependency_resolver.gleam", 128).
-spec should_skip(binary()) -> boolean().
should_skip(Module_path) ->
    gleam_stdlib:string_starts_with(Module_path, <<"generated/"/utf8>>) orelse (Module_path
    =:= <<"server_context"/utf8>>).

-file("src/rally/dependency_resolver.gleam", 41).
-spec resolve_loop(
    list({binary(), list(binary())}),
    gleam@set:set(binary()),
    binary(),
    binary(),
    list(rally@generator@client:generated_file())
) -> {ok, list(rally@generator@client:generated_file())} | {error, binary()}.
resolve_loop(Frontier, Visited, Src_root, Client_root, Acc) ->
    case Frontier of
        [] ->
            {ok, Acc};

        [{Module_path, Chain} | Rest] ->
            case gleam@set:contains(Visited, Module_path) orelse should_skip(
                Module_path
            ) of
                true ->
                    resolve_loop(Rest, Visited, Src_root, Client_root, Acc);

                false ->
                    File_path = <<<<<<Src_root/binary, "/"/utf8>>/binary,
                            Module_path/binary>>/binary,
                        ".gleam"/utf8>>,
                    Visited@1 = gleam@set:insert(Visited, Module_path),
                    case simplifile:read(File_path) of
                        {ok, Content} ->
                            case check_erlang_external(
                                Content,
                                Module_path,
                                Chain
                            ) of
                                {ok, _} ->
                                    Dest = <<<<<<Client_root/binary,
                                                "/src/"/utf8>>/binary,
                                            Module_path/binary>>/binary,
                                        ".gleam"/utf8>>,
                                    File = {generated_file, Dest, Content},
                                    Ffi_files = collect_ffi_files(
                                        Src_root,
                                        Client_root,
                                        Module_path
                                    ),
                                    New_chain = lists:append(
                                        Chain,
                                        [Module_path]
                                    ),
                                    New_imports = gleam@list:map(
                                        extract_imports(Content),
                                        fun(Imp) -> {Imp, New_chain} end
                                    ),
                                    resolve_loop(
                                        lists:append(Rest, New_imports),
                                        Visited@1,
                                        Src_root,
                                        Client_root,
                                        lists:append([File | Ffi_files], Acc)
                                    );

                                {error, Msg} ->
                                    {error, Msg}
                            end;

                        _ ->
                            resolve_loop(
                                Rest,
                                Visited@1,
                                Src_root,
                                Client_root,
                                Acc
                            )
                    end
            end
    end.

-file("src/rally/dependency_resolver.gleam", 18).
-spec resolve(list({binary(), binary()}), binary(), binary()) -> {ok,
        list(rally@generator@client:generated_file())} |
    {error, binary()}.
resolve(Seed_sources, Src_root, Client_root) ->
    Seed_modules = begin
        _pipe = gleam@list:map(
            Seed_sources,
            fun(Pair) -> erlang:element(1, Pair) end
        ),
        gleam@set:from_list(_pipe)
    end,
    Seed_imports = gleam@list:flat_map(
        Seed_sources,
        fun(Pair@1) ->
            gleam@list:map(
                extract_imports(erlang:element(2, Pair@1)),
                fun(Imp) -> {Imp, [erlang:element(1, Pair@1)]} end
            )
        end
    ),
    resolve_loop(Seed_imports, Seed_modules, Src_root, Client_root, []).