build.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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 generate_sitemap(deploy_url: str):
  47. """Generate the sitemap config file.
  48. Args:
  49. deploy_url: The URL of the deployed app.
  50. """
  51. # Import here to avoid circular imports.
  52. from pynecone.compiler import templates
  53. config = json.dumps(
  54. {
  55. "siteUrl": deploy_url,
  56. "generateRobotsTxt": True,
  57. }
  58. )
  59. with open(constants.SITEMAP_CONFIG_FILE, "w") as f:
  60. f.write(templates.SITEMAP_CONFIG(config=config))
  61. def export_app(
  62. app: App,
  63. backend: bool = True,
  64. frontend: bool = True,
  65. zip: bool = False,
  66. deploy_url: Optional[str] = None,
  67. loglevel: constants.LogLevel = constants.LogLevel.ERROR,
  68. ):
  69. """Zip up the app for deployment.
  70. Args:
  71. app: The app.
  72. backend: Whether to zip up the backend app.
  73. frontend: Whether to zip up the frontend app.
  74. zip: Whether to zip the app.
  75. deploy_url: The URL of the deployed app.
  76. loglevel: The log level to use.
  77. """
  78. # Remove the static folder.
  79. path_ops.rm(constants.WEB_STATIC_DIR)
  80. # Generate the sitemap file.
  81. if deploy_url is not None:
  82. generate_sitemap(deploy_url)
  83. # Create a progress object
  84. progress = Progress()
  85. # Add a single task to the progress object
  86. task = progress.add_task("Building app... ", total=500)
  87. # Start the progress bar
  88. with progress:
  89. # Run the subprocess command
  90. export_process = subprocess.Popen(
  91. [prerequisites.get_package_manager(), "run", "export"],
  92. cwd=constants.WEB_DIR,
  93. env=os.environ,
  94. stderr=subprocess.STDOUT,
  95. stdout=subprocess.PIPE, # Redirect stdout to a pipe
  96. universal_newlines=True, # Set universal_newlines to True for text mode
  97. )
  98. if export_process.stdout:
  99. for line in iter(export_process.stdout.readline, ""):
  100. if "Linting and checking " in line:
  101. progress.update(task, advance=100)
  102. elif "Compiled successfully" in line:
  103. progress.update(task, advance=100)
  104. elif "Route (pages)" in line:
  105. progress.update(task, advance=100)
  106. elif "automatically rendered as static HTML" in line:
  107. progress.update(task, advance=100)
  108. elif "Export successful" in line:
  109. print("DOOE")
  110. progress.update(task, completed=500)
  111. break # Exit the loop if the completion message is found
  112. elif loglevel == constants.LogLevel.DEBUG:
  113. print(line, end="")
  114. # Wait for the subprocess to complete
  115. export_process.wait()
  116. print("Export process completed.")
  117. # Zip up the app.
  118. if zip:
  119. if os.name == "posix":
  120. posix_export(backend, frontend)
  121. if os.name == "nt":
  122. nt_export(backend, frontend)
  123. def nt_export(backend: bool = True, frontend: bool = True):
  124. """Export for nt (Windows) systems.
  125. Args:
  126. backend: Whether to zip up the backend app.
  127. frontend: Whether to zip up the frontend app.
  128. """
  129. cmd = r""
  130. if frontend:
  131. cmd = r'''powershell -Command "Set-Location .web/_static; Compress-Archive -Path .\* -DestinationPath ..\..\frontend.zip -Force"'''
  132. os.system(cmd)
  133. if backend:
  134. cmd = r'''powershell -Command "Get-ChildItem -File | Where-Object { $_.Name -notin @('.web', 'assets', 'frontend.zip', 'backend.zip') } | Compress-Archive -DestinationPath backend.zip -Update"'''
  135. os.system(cmd)
  136. def posix_export(backend: bool = True, frontend: bool = True):
  137. """Export for posix (Linux, OSX) systems.
  138. Args:
  139. backend: Whether to zip up the backend app.
  140. frontend: Whether to zip up the frontend app.
  141. """
  142. cmd = r""
  143. if frontend:
  144. cmd = r"cd .web/_static && zip -r ../../frontend.zip ./*"
  145. os.system(cmd)
  146. if backend:
  147. cmd = r"zip -r backend.zip ./* -x .web/\* ./assets\* ./frontend.zip\* ./backend.zip\*"
  148. os.system(cmd)
  149. def setup_frontend(root: Path, disable_telemetry: bool = True):
  150. """Set up the frontend.
  151. Args:
  152. root: root path of the project.
  153. disable_telemetry: Whether to disable the Next telemetry.
  154. """
  155. # Initialize the web directory if it doesn't exist.
  156. web_dir = prerequisites.create_web_directory(root)
  157. # Install frontend packages.
  158. prerequisites.install_frontend_packages(web_dir)
  159. # Copy asset files to public folder.
  160. path_ops.mkdir(str(root / constants.WEB_ASSETS_DIR))
  161. path_ops.cp(
  162. src=str(root / constants.APP_ASSETS_DIR),
  163. dest=str(root / constants.WEB_ASSETS_DIR),
  164. )
  165. # set the environment variables in client(env.json)
  166. set_environment_variables()
  167. # Disable the Next telemetry.
  168. if disable_telemetry:
  169. subprocess.Popen(
  170. [
  171. prerequisites.get_package_manager(),
  172. "run",
  173. "next",
  174. "telemetry",
  175. "disable",
  176. ],
  177. cwd=constants.WEB_DIR,
  178. env=os.environ,
  179. stdout=subprocess.DEVNULL,
  180. stderr=subprocess.STDOUT,
  181. )
  182. def setup_backend():
  183. """Set up backend.
  184. Specifically ensures backend database is updated when running --no-frontend.
  185. """
  186. # Import here to avoid circular imports.
  187. from pynecone.model import Model
  188. config = get_config()
  189. if config.db_url is not None:
  190. Model.create_all()