build.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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 TYPE_CHECKING, Optional, Union
  9. from rich.progress import Progress
  10. from pynecone import constants
  11. from pynecone.config import get_config
  12. from pynecone.utils import path_ops, prerequisites
  13. if TYPE_CHECKING:
  14. from pynecone.app import App
  15. def update_json_file(file_path: str, update_dict: dict[str, Union[int, str]]):
  16. """Update the contents of a json file.
  17. Args:
  18. file_path: the path to the JSON file.
  19. update_dict: object to update json.
  20. """
  21. fp = Path(file_path)
  22. # create file if it doesn't exist
  23. fp.touch(exist_ok=True)
  24. # create an empty json object if file is empty
  25. fp.write_text("{}") if fp.stat().st_size == 0 else None
  26. with open(fp) as f: # type: ignore
  27. json_object: dict = json.load(f)
  28. json_object.update(update_dict)
  29. with open(fp, "w") as f:
  30. json.dump(json_object, f, ensure_ascii=False)
  31. def set_pynecone_project_hash():
  32. """Write the hash of the Pynecone project to a PCVERSION_APP_FILE."""
  33. update_json_file(
  34. constants.PCVERSION_APP_FILE, {"project_hash": random.getrandbits(128)}
  35. )
  36. def set_environment_variables():
  37. """Write the upload url to a PCVERSION_APP_FILE."""
  38. update_json_file(
  39. constants.ENV_JSON,
  40. {
  41. "uploadUrl": constants.Endpoint.UPLOAD.get_url(),
  42. "eventUrl": constants.Endpoint.EVENT.get_url(),
  43. "pingUrl": constants.Endpoint.PING.get_url(),
  44. },
  45. )
  46. def set_os_env(**kwargs):
  47. """Set os environment variables.
  48. Args:
  49. kwargs: env key word args.
  50. """
  51. for key, value in kwargs.items():
  52. if not value:
  53. continue
  54. os.environ[key.upper()] = value
  55. def generate_sitemap(deploy_url: str):
  56. """Generate the sitemap config file.
  57. Args:
  58. deploy_url: The URL of the deployed app.
  59. """
  60. # Import here to avoid circular imports.
  61. from pynecone.compiler import templates
  62. config = json.dumps(
  63. {
  64. "siteUrl": deploy_url,
  65. "generateRobotsTxt": True,
  66. }
  67. )
  68. with open(constants.SITEMAP_CONFIG_FILE, "w") as f:
  69. f.write(templates.SITEMAP_CONFIG(config=config))
  70. def export_app(
  71. app: App,
  72. backend: bool = True,
  73. frontend: bool = True,
  74. zip: bool = False,
  75. deploy_url: Optional[str] = None,
  76. loglevel: constants.LogLevel = constants.LogLevel.ERROR,
  77. ):
  78. """Zip up the app for deployment.
  79. Args:
  80. app: The app.
  81. backend: Whether to zip up the backend app.
  82. frontend: Whether to zip up the frontend app.
  83. zip: Whether to zip the app.
  84. deploy_url: The URL of the deployed app.
  85. loglevel: The log level to use.
  86. """
  87. # Remove the static folder.
  88. path_ops.rm(constants.WEB_STATIC_DIR)
  89. # Generate the sitemap file.
  90. if deploy_url is not None:
  91. generate_sitemap(deploy_url)
  92. # Create a progress object
  93. progress = Progress()
  94. # Add a single task to the progress object
  95. task = progress.add_task("Building app... ", total=500)
  96. # Start the progress bar
  97. with progress:
  98. # Run the subprocess command
  99. export_process = subprocess.Popen(
  100. [prerequisites.get_package_manager(), "run", "export"],
  101. cwd=constants.WEB_DIR,
  102. env=os.environ,
  103. stderr=subprocess.STDOUT,
  104. stdout=subprocess.PIPE, # Redirect stdout to a pipe
  105. universal_newlines=True, # Set universal_newlines to True for text mode
  106. )
  107. if export_process.stdout:
  108. for line in iter(export_process.stdout.readline, ""):
  109. if "Linting and checking " in line:
  110. progress.update(task, advance=100)
  111. elif "Compiled successfully" in line:
  112. progress.update(task, advance=100)
  113. elif "Route (pages)" in line:
  114. progress.update(task, advance=100)
  115. elif "automatically rendered as static HTML" in line:
  116. progress.update(task, advance=100)
  117. elif "Export successful" in line:
  118. print("DOOE")
  119. progress.update(task, completed=500)
  120. break # Exit the loop if the completion message is found
  121. elif loglevel == constants.LogLevel.DEBUG:
  122. print(line, end="")
  123. # Wait for the subprocess to complete
  124. export_process.wait()
  125. print("Export process completed.")
  126. # Zip up the app.
  127. if zip:
  128. if os.name == "posix":
  129. posix_export(backend, frontend)
  130. if os.name == "nt":
  131. nt_export(backend, frontend)
  132. def nt_export(backend: bool = True, frontend: bool = True):
  133. """Export for nt (Windows) systems.
  134. Args:
  135. backend: Whether to zip up the backend app.
  136. frontend: Whether to zip up the frontend app.
  137. """
  138. cmd = r""
  139. if frontend:
  140. cmd = r'''powershell -Command "Set-Location .web/_static; Compress-Archive -Path .\* -DestinationPath ..\..\frontend.zip -Force"'''
  141. os.system(cmd)
  142. if backend:
  143. cmd = r'''powershell -Command "Get-ChildItem -File | Where-Object { $_.Name -notin @('.web', 'assets', 'frontend.zip', 'backend.zip') } | Compress-Archive -DestinationPath backend.zip -Update"'''
  144. os.system(cmd)
  145. def posix_export(backend: bool = True, frontend: bool = True):
  146. """Export for posix (Linux, OSX) systems.
  147. Args:
  148. backend: Whether to zip up the backend app.
  149. frontend: Whether to zip up the frontend app.
  150. """
  151. cmd = r""
  152. if frontend:
  153. cmd = r"cd .web/_static && zip -r ../../frontend.zip ./*"
  154. os.system(cmd)
  155. if backend:
  156. cmd = r"zip -r backend.zip ./* -x .web/\* ./assets\* ./frontend.zip\* ./backend.zip\*"
  157. os.system(cmd)
  158. def setup_frontend(root: Path, disable_telemetry: bool = True):
  159. """Set up the frontend.
  160. Args:
  161. root: root path of the project.
  162. disable_telemetry: Whether to disable the Next telemetry.
  163. """
  164. # Initialize the web directory if it doesn't exist.
  165. web_dir = prerequisites.create_web_directory(root)
  166. # Install frontend packages.
  167. prerequisites.install_frontend_packages(web_dir)
  168. # Copy asset files to public folder.
  169. path_ops.cp(
  170. src=str(root / constants.APP_ASSETS_DIR),
  171. dest=str(root / constants.WEB_ASSETS_DIR),
  172. )
  173. # set the environment variables in client(env.json)
  174. set_environment_variables()
  175. # Disable the Next telemetry.
  176. if disable_telemetry:
  177. subprocess.Popen(
  178. [
  179. prerequisites.get_package_manager(),
  180. "run",
  181. "next",
  182. "telemetry",
  183. "disable",
  184. ],
  185. cwd=constants.WEB_DIR,
  186. env=os.environ,
  187. stdout=subprocess.DEVNULL,
  188. stderr=subprocess.STDOUT,
  189. )
  190. def setup_backend():
  191. """Set up backend.
  192. Specifically ensures backend database is updated when running --no-frontend.
  193. """
  194. # Import here to avoid circular imports.
  195. from pynecone.model import Model
  196. config = get_config()
  197. if config.db_url is not None:
  198. Model.create_all()