summaryrefslogtreecommitdiff
path: root/bs5/packages/extract-client-components/extract_client_components.ml
blob: d9a6953d830b605cdd4077fca71c2dcf9214c861 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
module List = ListLabels

let read_file path = try Some (In_channel.with_open_bin path In_channel.input_all) with _ -> None

type manifest_item =
  | Client_component of { original_path : string; compiled_js_path : string; module_name : string list option }
  | Server_function of {
      id : string;
      compiled_js_path : string;
      module_name : string list option;
      function_name : string;
    }

let parse_module_name str = String.split_on_char '.' str
let print_module_name str = String.concat "." str

let parse_client_component_line line =
  try
    Scanf.sscanf line "// extract-client %s %s" (fun filename module_name ->
        Ok (filename, if module_name = "" then None else Some (parse_module_name module_name)))
  with End_of_file | Scanf.Scan_failure _ -> Error "Invalid `extract-client` command format"

let parse_server_function_line line =
  try
    Scanf.sscanf line "// extract-server-function %s %s %s" (fun id function_name module_name ->
        Ok ((if module_name = "" then None else Some (parse_module_name module_name)), function_name, id))
  with End_of_file | Scanf.Scan_failure _ -> Error "Invalid `extract-server-function` command format"

let parse_manifest_item ~path line =
  match (parse_client_component_line (String.trim line), parse_server_function_line (String.trim line)) with
  | Ok (original_path, module_name), _ ->
      Some (Client_component { compiled_js_path = path; original_path; module_name })
  | _, Ok (module_name, function_name, id) ->
      Some (Server_function { compiled_js_path = path; module_name; function_name; id })
  | Error _, Error _ -> None

let parse_manifest_data ~path content : manifest_item list =
  content |> String.split_on_char '\n' |> List.filter_map ~f:(parse_manifest_item ~path)

let render_manifest manifest =
  let register_client_modules =
    List.map manifest ~f:(function
      | Client_component { original_path; compiled_js_path; module_name } ->
          let original_path_with_submodule =
            match module_name with
            | Some name -> Printf.sprintf "%s#%s" original_path (print_module_name name)
            | None -> original_path
          in
          let export =
            match module_name with
            | Some name -> Printf.sprintf "%s.make_client" (print_module_name name)
            | None -> "make_client"
          in
          Printf.sprintf
            "window.__client_manifest_map[\"%s\"] = React.lazy(() => import(\"%s\").then(module => {\n\
            \  return { default: module.%s }\n\
             }).catch(err => { console.error(err); return { default: null }; }))"
            original_path_with_submodule compiled_js_path export
      | Server_function { compiled_js_path; module_name; function_name; id } ->
          let export =
            match module_name with
            | Some name -> Printf.sprintf "%s.%s" (print_module_name name) function_name
            | None -> function_name
          in
          Printf.sprintf "window.__server_functions_manifest_map[\"%s\"] = require(\"%s\").%s" id compiled_js_path
            export)
  in
  Printf.sprintf
    {|import React from "react";
window.__client_manifest_map = window.__client_manifest_map || {};
window.__server_functions_manifest_map = window.__server_functions_manifest_map || {};
%s|}
    (String.concat "\n" register_client_modules)

(* TODO: Add parameter to allow users to configure the extension of the files *)
let is_js_file path =
  let ext = Filename.extension path in
  ext = ".js" || ext = ".bs.js" || ext = ".jsx"

(* TODO: refactor path to be a Filepath, not a string *)
let capture_all_client_modules_files_in_target path =
  let rec traverse_fs path =
    try
      match Sys.is_directory path with
      | true ->
          let contents = Sys.readdir path in
          Array.fold_left
            (fun acc entry ->
              let full_path = Filename.concat path entry in
              match acc with
              | Ok files -> (
                  match traverse_fs full_path with Ok new_files -> Ok (files @ new_files) | Error err -> Error err)
              | Error err -> Error err)
            (Ok []) contents
      | false ->
          if is_js_file path then
            match read_file path with
            | Some content -> Ok (parse_manifest_data ~path content)
            | None -> Error (Printf.sprintf "Failed to read file: %s" path)
          else Ok []
    with
    | Sys_error msg -> Error (Printf.sprintf "System error: %s" msg)
    | Unix.Unix_error (err, _, _) -> Error (Printf.sprintf "Unix error: %s" (Unix.error_message err))
    | e -> Error (Printf.sprintf "Unexpected error: %s" (Printexc.to_string e))
  in
  traverse_fs path

let melange_target =
  let doc = "Path to the melange target directory (melange.emit (target xxx))" in
  Cmdliner.Arg.(required & pos 0 (some string) None & info [] ~docv:"MELANGE_TARGET" ~doc)

let extract_modules target =
  let current_dir = Sys.getcwd () in
  let melange_target = Filename.concat current_dir target in
  match capture_all_client_modules_files_in_target melange_target with
  | Ok manifest ->
      print_endline (render_manifest manifest);
      Ok ()
  | Error msg -> Error (`Msg msg)

let extract_cmd =
  let open Cmdliner in
  let doc = "Extract all client modules from a Melange target folder" in
  let sdocs = Manpage.s_common_options in
  let info = Cmd.info "extract-client-components" ~version:"1.0.0" ~doc ~sdocs in
  let term = Term.(term_result (const extract_modules $ melange_target)) in
  Cmd.v info term

let () = exit (Cmdliner.Cmd.eval extract_cmd)