build.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. """Building the app and initializing all prerequisites."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import random
  6. import subprocess
  7. from pathlib import Path
  8. from typing import Optional, Union
  9. from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
  10. from reflex import constants
  11. from reflex.config import get_config
  12. from reflex.utils import console, path_ops, prerequisites
  13. from reflex.utils.processes import new_process
  14. def update_json_file(file_path: str, update_dict: dict[str, Union[int, str]]):
  15. """Update the contents of a json file.
  16. Args:
  17. file_path: the path to the JSON file.
  18. update_dict: object to update json.
  19. """
  20. fp = Path(file_path)
  21. # create file if it doesn't exist
  22. fp.touch(exist_ok=True)
  23. # create an empty json object if file is empty
  24. fp.write_text("{}") if fp.stat().st_size == 0 else None
  25. with open(fp) as f: # type: ignore
  26. json_object: dict = json.load(f)
  27. json_object.update(update_dict)
  28. with open(fp, "w") as f:
  29. json.dump(json_object, f, ensure_ascii=False)
  30. def set_reflex_project_hash():
  31. """Write the hash of the Reflex project to a REFLEX_JSON."""
  32. update_json_file(constants.REFLEX_JSON, {"project_hash": random.getrandbits(128)})
  33. def set_environment_variables():
  34. """Write the upload url to a REFLEX_JSON."""
  35. update_json_file(
  36. constants.ENV_JSON,
  37. {
  38. "uploadUrl": constants.Endpoint.UPLOAD.get_url(),
  39. "eventUrl": constants.Endpoint.EVENT.get_url(),
  40. "pingUrl": constants.Endpoint.PING.get_url(),
  41. },
  42. )
  43. def set_os_env(**kwargs):
  44. """Set os environment variables.
  45. Args:
  46. kwargs: env key word args.
  47. """
  48. for key, value in kwargs.items():
  49. if not value:
  50. continue
  51. os.environ[key.upper()] = value
  52. def generate_sitemap_config(deploy_url: str):
  53. """Generate the sitemap config file.
  54. Args:
  55. deploy_url: The URL of the deployed app.
  56. """
  57. # Import here to avoid circular imports.
  58. from reflex.compiler import templates
  59. config = json.dumps(
  60. {
  61. "siteUrl": deploy_url,
  62. "generateRobotsTxt": True,
  63. }
  64. )
  65. with open(constants.SITEMAP_CONFIG_FILE, "w") as f:
  66. f.write(templates.SITEMAP_CONFIG(config=config))
  67. def export_app(
  68. backend: bool = True,
  69. frontend: bool = True,
  70. zip: bool = False,
  71. deploy_url: Optional[str] = None,
  72. loglevel: constants.LogLevel = constants.LogLevel.ERROR,
  73. ):
  74. """Zip up the app for deployment.
  75. Args:
  76. backend: Whether to zip up the backend app.
  77. frontend: Whether to zip up the frontend app.
  78. zip: Whether to zip the app.
  79. deploy_url: The URL of the deployed app.
  80. loglevel: The log level to use.
  81. """
  82. # Remove the static folder.
  83. path_ops.rm(constants.WEB_STATIC_DIR)
  84. # Generate the sitemap file.
  85. command = "export"
  86. if deploy_url is not None:
  87. generate_sitemap_config(deploy_url)
  88. command = "export-sitemap"
  89. # Create a progress object
  90. progress = Progress(
  91. *Progress.get_default_columns()[:-1],
  92. MofNCompleteColumn(),
  93. TimeElapsedColumn(),
  94. )
  95. checkpoints = [
  96. "Linting and checking ",
  97. "Compiled successfully",
  98. "Route (pages)",
  99. "Collecting page data",
  100. "automatically rendered as static HTML",
  101. 'Copying "static build" directory',
  102. 'Copying "public" directory',
  103. "Finalizing page optimization",
  104. "Export successful",
  105. ]
  106. # Add a single task to the progress object
  107. task = progress.add_task("Creating Production Build: ", total=len(checkpoints))
  108. # Start the subprocess with the progress bar.
  109. try:
  110. with progress, new_process(
  111. [prerequisites.get_package_manager(), "run", command],
  112. cwd=constants.WEB_DIR,
  113. ) as export_process:
  114. assert export_process.stdout is not None, "No stdout for export process."
  115. for line in export_process.stdout:
  116. if loglevel == constants.LogLevel.DEBUG:
  117. print(line, end="")
  118. # Check for special strings and update the progress bar.
  119. for special_string in checkpoints:
  120. if special_string in line:
  121. if special_string == checkpoints[-1]:
  122. progress.update(task, completed=len(checkpoints))
  123. break # Exit the loop if the completion message is found
  124. else:
  125. progress.update(task, advance=1)
  126. break
  127. except Exception as e:
  128. console.print(f"[red]Export process errored: {e}")
  129. console.print(
  130. "[red]Run in with [bold]--loglevel debug[/bold] to see the full error."
  131. )
  132. os._exit(1)
  133. # Zip up the app.
  134. if zip:
  135. if os.name == "posix":
  136. posix_export(backend, frontend)
  137. if os.name == "nt":
  138. nt_export(backend, frontend)
  139. def nt_export(backend: bool = True, frontend: bool = True):
  140. """Export for nt (Windows) systems.
  141. Args:
  142. backend: Whether to zip up the backend app.
  143. frontend: Whether to zip up the frontend app.
  144. """
  145. cmd = r""
  146. if frontend:
  147. cmd = r'''powershell -Command "Set-Location .web/_static; Compress-Archive -Path .\* -DestinationPath ..\..\frontend.zip -Force"'''
  148. os.system(cmd)
  149. if backend:
  150. cmd = r'''powershell -Command "Get-ChildItem -File | Where-Object { $_.Name -notin @('.web', 'assets', 'frontend.zip', 'backend.zip') } | Compress-Archive -DestinationPath backend.zip -Update"'''
  151. os.system(cmd)
  152. def posix_export(backend: bool = True, frontend: bool = True):
  153. """Export for posix (Linux, OSX) systems.
  154. Args:
  155. backend: Whether to zip up the backend app.
  156. frontend: Whether to zip up the frontend app.
  157. """
  158. cmd = r""
  159. if frontend:
  160. cmd = r"cd .web/_static && zip -r ../../frontend.zip ./*"
  161. os.system(cmd)
  162. if backend:
  163. cmd = r"zip -r backend.zip ./* -x .web/\* ./assets\* ./frontend.zip\* ./backend.zip\*"
  164. os.system(cmd)
  165. def setup_frontend(
  166. root: Path,
  167. loglevel: constants.LogLevel = constants.LogLevel.ERROR,
  168. disable_telemetry: bool = True,
  169. ):
  170. """Set up the frontend to run the app.
  171. Args:
  172. root: The root path of the project.
  173. loglevel: The log level to use.
  174. disable_telemetry: Whether to disable the Next telemetry.
  175. """
  176. # Install frontend packages.
  177. prerequisites.install_frontend_packages()
  178. # Copy asset files to public folder.
  179. path_ops.cp(
  180. src=str(root / constants.APP_ASSETS_DIR),
  181. dest=str(root / constants.WEB_ASSETS_DIR),
  182. )
  183. # Set the environment variables in client (env.json).
  184. set_environment_variables()
  185. # Disable the Next telemetry.
  186. if disable_telemetry:
  187. new_process(
  188. [
  189. prerequisites.get_package_manager(),
  190. "run",
  191. "next",
  192. "telemetry",
  193. "disable",
  194. ],
  195. cwd=constants.WEB_DIR,
  196. stdout=subprocess.DEVNULL,
  197. )
  198. def setup_frontend_prod(
  199. root: Path,
  200. loglevel: constants.LogLevel = constants.LogLevel.ERROR,
  201. disable_telemetry: bool = True,
  202. ):
  203. """Set up the frontend for prod mode.
  204. Args:
  205. root: The root path of the project.
  206. loglevel: The log level to use.
  207. disable_telemetry: Whether to disable the Next telemetry.
  208. """
  209. setup_frontend(root, loglevel, disable_telemetry)
  210. export_app(loglevel=loglevel, deploy_url=get_config().deploy_url)