瀏覽代碼

Add support for custom components starter (#2314)

Martin Xu 1 年之前
父節點
當前提交
4f9cdd6472

+ 1 - 1
.coveragerc

@@ -5,7 +5,7 @@ branch = true
 [report]
 show_missing = true
 # TODO bump back to 79
-fail_under = 69
+fail_under = 68
 precision = 2
 
 # Regexes for lines to exclude from consideration

+ 63 - 118
poetry.lock

@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
 
 [[package]]
 name = "alembic"
 version = "1.13.1"
 description = "A database migration tool for SQLAlchemy."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -26,7 +25,6 @@ tz = ["backports.zoneinfo"]
 name = "anyio"
 version = "4.3.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -49,7 +47,6 @@ trio = ["trio (>=0.23)"]
 name = "async-timeout"
 version = "4.0.3"
 description = "Timeout context manager for asyncio programs"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -61,7 +58,6 @@ files = [
 name = "asynctest"
 version = "0.13.0"
 description = "Enhance the standard unittest package with features for testing asyncio libraries"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -73,7 +69,6 @@ files = [
 name = "attrs"
 version = "23.2.0"
 description = "Classes Without Boilerplate"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -93,7 +88,6 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p
 name = "bidict"
 version = "0.23.1"
 description = "The bidirectional mapping library for Python."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -105,7 +99,6 @@ files = [
 name = "black"
 version = "22.12.0"
 description = "The uncompromising code formatter."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -137,11 +130,34 @@ d = ["aiohttp (>=3.7.4)"]
 jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
 uvloop = ["uvloop (>=0.15.2)"]
 
+[[package]]
+name = "build"
+version = "1.0.3"
+description = "A simple, correct Python build frontend"
+optional = false
+python-versions = ">= 3.7"
+files = [
+    {file = "build-1.0.3-py3-none-any.whl", hash = "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f"},
+    {file = "build-1.0.3.tar.gz", hash = "sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "os_name == \"nt\""}
+importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
+packaging = ">=19.0"
+pyproject_hooks = "*"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"]
+test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"]
+typing = ["importlib-metadata (>=5.1)", "mypy (>=1.5.0,<1.6.0)", "tomli", "typing-extensions (>=3.7.4.3)"]
+virtualenv = ["virtualenv (>=20.0.35)"]
+
 [[package]]
 name = "certifi"
 version = "2024.2.2"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -153,7 +169,6 @@ files = [
 name = "cffi"
 version = "1.16.0"
 description = "Foreign Function Interface for Python calling C code."
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -218,7 +233,6 @@ pycparser = "*"
 name = "cfgv"
 version = "3.4.0"
 description = "Validate configuration and produce human readable error messages."
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -230,7 +244,6 @@ files = [
 name = "charset-normalizer"
 version = "3.3.2"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -330,7 +343,6 @@ files = [
 name = "click"
 version = "8.1.7"
 description = "Composable command line interface toolkit"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -345,7 +357,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
 name = "cloudpickle"
 version = "2.2.1"
 description = "Extended pickling support for Python objects"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -357,7 +368,6 @@ files = [
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -369,7 +379,6 @@ files = [
 name = "coverage"
 version = "7.4.1"
 description = "Code coverage measurement for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -437,7 +446,6 @@ toml = ["tomli"]
 name = "darglint"
 version = "1.8.1"
 description = "A utility for ensuring Google-style docstrings stay up to date with the source code."
-category = "dev"
 optional = false
 python-versions = ">=3.6,<4.0"
 files = [
@@ -449,7 +457,6 @@ files = [
 name = "distlib"
 version = "0.3.8"
 description = "Distribution utilities"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -461,7 +468,6 @@ files = [
 name = "distro"
 version = "1.9.0"
 description = "Distro - an OS platform information API"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -473,7 +479,6 @@ files = [
 name = "docopt"
 version = "0.6.2"
 description = "Pythonic argument parser, that will make you smile"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -484,7 +489,6 @@ files = [
 name = "exceptiongroup"
 version = "1.2.0"
 description = "Backport of PEP 654 (exception groups)"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -499,7 +503,6 @@ test = ["pytest (>=6)"]
 name = "fastapi"
 version = "0.96.1"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -521,7 +524,6 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6
 name = "filelock"
 version = "3.13.1"
 description = "A platform independent file lock."
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -538,7 +540,6 @@ typing = ["typing-extensions (>=4.8)"]
 name = "greenlet"
 version = "3.0.3"
 description = "Lightweight in-process concurrent programming"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -610,7 +611,6 @@ test = ["objgraph", "psutil"]
 name = "gunicorn"
 version = "20.1.0"
 description = "WSGI HTTP Server for UNIX"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -631,7 +631,6 @@ tornado = ["tornado (>=0.2)"]
 name = "h11"
 version = "0.14.0"
 description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -643,7 +642,6 @@ files = [
 name = "httpcore"
 version = "1.0.3"
 description = "A minimal low-level HTTP client."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -658,14 +656,13 @@ h11 = ">=0.13,<0.15"
 [package.extras]
 asyncio = ["anyio (>=4.0,<5.0)"]
 http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (>=1.0.0,<2.0.0)"]
+socks = ["socksio (==1.*)"]
 trio = ["trio (>=0.22.0,<0.24.0)"]
 
 [[package]]
 name = "httpx"
 version = "0.25.2"
 description = "The next generation HTTP client."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -676,21 +673,20 @@ files = [
 [package.dependencies]
 anyio = "*"
 certifi = "*"
-httpcore = ">=1.0.0,<2.0.0"
+httpcore = "==1.*"
 idna = "*"
 sniffio = "*"
 
 [package.extras]
 brotli = ["brotli", "brotlicffi"]
-cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
 http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (>=1.0.0,<2.0.0)"]
+socks = ["socksio (==1.*)"]
 
 [[package]]
 name = "identify"
 version = "2.5.35"
 description = "File identification library for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -705,7 +701,6 @@ license = ["ukkonen"]
 name = "idna"
 version = "3.6"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -717,7 +712,6 @@ files = [
 name = "importlib-metadata"
 version = "7.0.1"
 description = "Read metadata from Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -737,7 +731,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
 name = "importlib-resources"
 version = "6.1.1"
 description = "Read resources from Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -756,7 +749,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)",
 name = "iniconfig"
 version = "2.0.0"
 description = "brain-dead simple config-ini parsing"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -768,7 +760,6 @@ files = [
 name = "jinja2"
 version = "3.1.3"
 description = "A very fast and expressive template engine."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -786,7 +777,6 @@ i18n = ["Babel (>=2.7)"]
 name = "mako"
 version = "1.3.2"
 description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -806,7 +796,6 @@ testing = ["pytest"]
 name = "markdown-it-py"
 version = "3.0.0"
 description = "Python port of markdown-it. Markdown parsing, done right!"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -831,7 +820,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 name = "markupsafe"
 version = "2.1.5"
 description = "Safely add untrusted strings to HTML/XML markup."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -901,7 +889,6 @@ files = [
 name = "mdurl"
 version = "0.1.2"
 description = "Markdown URL utilities"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -913,7 +900,6 @@ files = [
 name = "mypy-extensions"
 version = "1.0.0"
 description = "Type system extensions for programs checked with the mypy type checker."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -925,7 +911,6 @@ files = [
 name = "nodeenv"
 version = "1.8.0"
 description = "Node.js virtual environment builder"
-category = "dev"
 optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
 files = [
@@ -940,7 +925,6 @@ setuptools = "*"
 name = "numpy"
 version = "1.24.4"
 description = "Fundamental package for array computing in Python"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -978,7 +962,6 @@ files = [
 name = "numpy"
 version = "1.26.4"
 description = "Fundamental package for array computing in Python"
-category = "dev"
 optional = false
 python-versions = ">=3.9"
 files = [
@@ -1024,7 +1007,6 @@ files = [
 name = "outcome"
 version = "1.3.0.post0"
 description = "Capture the outcome of Python function calls."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1039,7 +1021,6 @@ attrs = ">=19.2.0"
 name = "packaging"
 version = "23.2"
 description = "Core utilities for Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1051,7 +1032,6 @@ files = [
 name = "pandas"
 version = "1.5.3"
 description = "Powerful data structures for data analysis, time series, and statistics"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1096,7 +1076,6 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
 name = "pandas"
 version = "2.2.0"
 description = "Powerful data structures for data analysis, time series, and statistics"
-category = "dev"
 optional = false
 python-versions = ">=3.9"
 files = [
@@ -1169,7 +1148,6 @@ xml = ["lxml (>=4.9.2)"]
 name = "pathspec"
 version = "0.12.1"
 description = "Utility library for gitignore style pattern matching of file paths."
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1181,7 +1159,6 @@ files = [
 name = "pillow"
 version = "10.2.0"
 description = "Python Imaging Library (Fork)"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1267,7 +1244,6 @@ xmp = ["defusedxml"]
 name = "pipdeptree"
 version = "2.14.0"
 description = "Command line utility to show dependency tree of packages."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1283,7 +1259,6 @@ test = ["covdefaults (>=2.3)", "diff-cover (>=8.0.1)", "pip (>=23.3.1)", "pytest
 name = "pipreqs"
 version = "0.4.13"
 description = "Pip requirements.txt generator based on imports in project"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1299,7 +1274,6 @@ yarg = "*"
 name = "platformdirs"
 version = "3.11.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1315,7 +1289,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
 name = "plotly"
 version = "5.19.0"
 description = "An open-source, interactive data visualization library for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1331,7 +1304,6 @@ tenacity = ">=6.2.0"
 name = "pluggy"
 version = "1.4.0"
 description = "plugin and hook calling mechanisms for python"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1347,7 +1319,6 @@ testing = ["pytest", "pytest-benchmark"]
 name = "pre-commit"
 version = "3.5.0"
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1366,7 +1337,6 @@ virtualenv = ">=20.10.0"
 name = "psutil"
 version = "5.9.8"
 description = "Cross-platform lib for process and system monitoring in Python."
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
@@ -1395,7 +1365,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
 name = "py-cpuinfo"
 version = "9.0.0"
 description = "Get CPU info with pure Python"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1407,7 +1376,6 @@ files = [
 name = "pycparser"
 version = "2.21"
 description = "C parser in Python"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -1419,7 +1387,6 @@ files = [
 name = "pydantic"
 version = "1.10.14"
 description = "Data validation and settings management using python type hints"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1472,7 +1439,6 @@ email = ["email-validator (>=1.0.3)"]
 name = "pygments"
 version = "2.17.2"
 description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1484,11 +1450,24 @@ files = [
 plugins = ["importlib-metadata"]
 windows-terminal = ["colorama (>=0.4.6)"]
 
+[[package]]
+name = "pyproject-hooks"
+version = "1.0.0"
+description = "Wrappers to call pyproject.toml-based build backend hooks."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"},
+    {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"},
+]
+
+[package.dependencies]
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
 [[package]]
 name = "pyright"
 version = "1.1.334"
 description = "Command line wrapper for pyright"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1507,7 +1486,6 @@ dev = ["twine (>=3.4.1)"]
 name = "pysocks"
 version = "1.7.1"
 description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -1520,7 +1498,6 @@ files = [
 name = "pytest"
 version = "7.4.4"
 description = "pytest: simple powerful testing with Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1543,7 +1520,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
 name = "pytest-asyncio"
 version = "0.20.3"
 description = "Pytest support for asyncio"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1562,7 +1538,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
 name = "pytest-benchmark"
 version = "4.0.0"
 description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1583,7 +1558,6 @@ histogram = ["pygal", "pygaljs"]
 name = "pytest-cov"
 version = "4.1.0"
 description = "Pytest plugin for measuring coverage."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1602,7 +1576,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
 name = "pytest-mock"
 version = "3.12.0"
 description = "Thin-wrapper around the mock package for easier use with pytest"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1620,7 +1593,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
 name = "python-dateutil"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
@@ -1635,7 +1607,6 @@ six = ">=1.5"
 name = "python-engineio"
 version = "4.9.0"
 description = "Engine.IO server and client for Python"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1655,7 +1626,6 @@ docs = ["sphinx"]
 name = "python-multipart"
 version = "0.0.5"
 description = "A streaming multipart parser for Python"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1669,7 +1639,6 @@ six = ">=1.4.0"
 name = "python-socketio"
 version = "5.11.1"
 description = "Socket.IO server and client for Python"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1690,7 +1659,6 @@ docs = ["sphinx"]
 name = "pytz"
 version = "2024.1"
 description = "World timezone definitions, modern and historical"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1702,7 +1670,6 @@ files = [
 name = "pyyaml"
 version = "6.0.1"
 description = "YAML parser and emitter for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1752,7 +1719,6 @@ files = [
 name = "redis"
 version = "4.6.0"
 description = "Python client for Redis database and key-value store"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1771,7 +1737,6 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
 name = "reflex-hosting-cli"
 version = "0.1.8"
 description = "Reflex Hosting CLI"
-category = "main"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
@@ -1796,7 +1761,6 @@ websockets = ">=10.4"
 name = "requests"
 version = "2.31.0"
 description = "Python HTTP for Humans."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1818,7 +1782,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 name = "rich"
 version = "13.7.0"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -1838,7 +1801,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
 name = "ruff"
 version = "0.0.244"
 description = "An extremely fast Python linter, written in Rust."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1864,7 +1826,6 @@ files = [
 name = "selenium"
 version = "4.18.0"
 description = ""
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1881,26 +1842,24 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
 
 [[package]]
 name = "setuptools"
-version = "69.1.0"
+version = "69.1.1"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"},
-    {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"},
+    {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
+    {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
 ]
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "simple-websocket"
 version = "1.0.0"
 description = "Simple WebSocket server and client for Python"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1918,7 +1877,6 @@ docs = ["sphinx"]
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -1930,7 +1888,6 @@ files = [
 name = "sniffio"
 version = "1.3.0"
 description = "Sniff out which async library your code is running under"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1942,7 +1899,6 @@ files = [
 name = "sortedcontainers"
 version = "2.4.0"
 description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1954,7 +1910,6 @@ files = [
 name = "sqlalchemy"
 version = "2.0.27"
 description = "Database Abstraction Library"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2042,7 +1997,6 @@ sqlcipher = ["sqlcipher3_binary"]
 name = "sqlmodel"
 version = "0.0.14"
 description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
-category = "main"
 optional = false
 python-versions = ">=3.7,<4.0"
 files = [
@@ -2058,7 +2012,6 @@ SQLAlchemy = ">=2.0.0,<2.1.0"
 name = "starlette"
 version = "0.27.0"
 description = "The little ASGI library that shines."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2077,7 +2030,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam
 name = "starlette-admin"
 version = "0.9.0"
 description = "Fast, beautiful and extensible administrative interface framework for Starlette/FastApi applications"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2100,7 +2052,6 @@ test = ["aiomysql (>=0.1.1,<0.2.0)", "aiosqlite (>=0.17.0,<0.20.0)", "arrow (>=1
 name = "tabulate"
 version = "0.9.0"
 description = "Pretty-print tabular data"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2115,7 +2066,6 @@ widechars = ["wcwidth"]
 name = "tenacity"
 version = "8.2.3"
 description = "Retry code until it succeeds"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2130,7 +2080,6 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"]
 name = "toml"
 version = "0.10.2"
 description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -2142,7 +2091,6 @@ files = [
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2154,7 +2102,6 @@ files = [
 name = "trio"
 version = "0.24.0"
 description = "A friendly Python library for async concurrency and I/O"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2175,7 +2122,6 @@ sortedcontainers = "*"
 name = "trio-websocket"
 version = "0.11.1"
 description = "WebSocket library for Trio"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2192,7 +2138,6 @@ wsproto = ">=0.14"
 name = "typer"
 version = "0.9.0"
 description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2214,7 +2159,6 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.
 name = "types-tabulate"
 version = "0.9.0.20240106"
 description = "Typing stubs for tabulate"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2226,7 +2170,6 @@ files = [
 name = "typing-extensions"
 version = "4.9.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2238,7 +2181,6 @@ files = [
 name = "tzdata"
 version = "2024.1"
 description = "Provider of IANA time zone data"
-category = "dev"
 optional = false
 python-versions = ">=2"
 files = [
@@ -2250,7 +2192,6 @@ files = [
 name = "urllib3"
 version = "2.2.1"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2271,7 +2212,6 @@ zstd = ["zstandard (>=0.18.0)"]
 name = "uvicorn"
 version = "0.20.0"
 description = "The lightning-fast ASGI server."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2290,7 +2230,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
 name = "uvicorn"
 version = "0.24.0.post1"
 description = "The lightning-fast ASGI server."
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2309,7 +2248,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
 name = "virtualenv"
 version = "20.25.0"
 description = "Virtual Python Environment builder"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2330,7 +2268,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
 name = "watchdog"
 version = "2.3.1"
 description = "Filesystem events monitoring"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2371,7 +2308,6 @@ watchmedo = ["PyYAML (>=3.10)"]
 name = "watchfiles"
 version = "0.19.0"
 description = "Simple, modern and high performance file watching and code reload in python."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2406,7 +2342,6 @@ anyio = ">=3.0.0"
 name = "websockets"
 version = "12.0"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2484,11 +2419,24 @@ files = [
     {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
 ]
 
+[[package]]
+name = "wheel"
+version = "0.42.0"
+description = "A built-package format for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "wheel-0.42.0-py3-none-any.whl", hash = "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d"},
+    {file = "wheel-0.42.0.tar.gz", hash = "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"},
+]
+
+[package.extras]
+test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
+
 [[package]]
 name = "wrapt"
 version = "1.16.0"
 description = "Module for decorators, wrappers and monkey patching."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2568,7 +2516,6 @@ files = [
 name = "wsproto"
 version = "1.2.0"
 description = "WebSockets state-machine based protocol implementation"
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -2583,7 +2530,6 @@ h11 = ">=0.9.0,<1"
 name = "yarg"
 version = "0.1.9"
 description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2598,7 +2544,6 @@ requests = "*"
 name = "zipp"
 version = "3.17.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2613,4 +2558,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "c22317cf6beac82268e73619e984788895d258bb7b7983eacdfbf6093c419dc3"
+content-hash = "840901e82824445e708eb44ae7f237b286946db2dd3332740c6d1e5e891fb84d"

+ 3 - 0
pyproject.toml

@@ -59,6 +59,9 @@ packaging = "^23.1"
 pipdeptree = "^2.13.0"
 reflex-hosting-cli = ">=0.1.2"
 charset-normalizer = "^3.3.2"
+wheel = "^0.42.0"
+build = "^1.0.3"
+setuptools = "^69.1.1"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.2"

+ 9 - 0
reflex/.templates/jinja/custom_components/README.md.jinja2

@@ -0,0 +1,9 @@
+# {{ module_name }}
+
+A Reflex custom component {{ module_name }}.
+
+## Installation
+
+```bash
+pip install {{ package_name }}
+```

+ 1 - 0
reflex/.templates/jinja/custom_components/__init__.py.jinja2

@@ -0,0 +1 @@
+from .{{ module_name }} import *

+ 36 - 0
reflex/.templates/jinja/custom_components/demo_app.py.jinja2

@@ -0,0 +1,36 @@
+"""Welcome to Reflex! This file showcases the custom component in a basic app."""
+
+from rxconfig import config
+
+import reflex as rx
+
+from {{ custom_component_module_dir }} import {{ module_name }}
+
+filename = f"{config.app_name}/{config.app_name}.py"
+
+
+class State(rx.State):
+    """The app state."""
+
+    pass
+
+
+def index() -> rx.Component:
+    return rx.center(
+        rx.theme_panel(),
+        rx.vstack(
+            rx.heading("Welcome to Reflex!", size="9"),
+            rx.text("Test your custom component by editing ", rx.code(filename)),
+            {{ module_name }}(),
+            align="center",
+            spacing="7",
+            font_size="2em",
+        ),
+        height="100vh",
+    )
+
+
+# Add state and page to the app.
+app = rx.App()
+app.add_page(index)
+

+ 35 - 0
reflex/.templates/jinja/custom_components/pyproject.toml.jinja2

@@ -0,0 +1,35 @@
+[build-system]
+requires = [
+    "setuptools",
+    "wheel",
+]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "{{ package_name }}"
+version = "0.0.1"
+description = "Reflex custom component {{ module_name }}"
+readme = "README.md"
+license = { text = "Apache-2.0" }
+requires-python = ">=3.8"
+authors = [{ name = "Your Name", email = "YOUREMAIL@domain.com" }]
+keywords = [
+    "reflex",
+    "reflex-custom-components"]
+
+dependencies = [
+    "reflex>=0.4.2"
+]
+
+classifiers = [
+  "Development Status :: 4 - Beta",
+]
+
+[project.urls]
+Homepage = "https://github.com"
+
+[project.optional-dependencies]
+dev = ["build", "twine"]
+
+[tool.setuptools.packages.find]
+where = ["custom_components"]

+ 57 - 0
reflex/.templates/jinja/custom_components/src.py.jinja2

@@ -0,0 +1,57 @@
+"""Reflex custom component {{ component_class_name }}."""
+
+# For wrapping react guide, visit https://reflex.dev/docs/wrapping-react/overview/
+
+import reflex as rx
+
+# Some libraries you may want to wrap may require dynamic imports.
+# This is because they they may not be compatible with Server-Side Rendering (SSR).
+# To handle this in Reflex all you need to do is subclass NoSSRComponent instead.
+# For example:
+# from reflex.components.component import NoSSRComponent
+# class {{ component_class_name }}(NoSSRComponent):
+#     pass
+
+
+class {{ component_class_name }}(rx.Component):
+    """{{ component_class_name }} component."""
+
+    # The React library to wrap.
+    library = "Fill-Me"
+
+    # The React component tag.
+    tag = "Fill-Me"
+
+    # If the tag is the default export from the module, you must set is_default = True.
+    # This is normally used when components don't have curly braces around them when importing.
+    # is_default = True
+
+    # If you are wrapping another components with the same tag as a component in your project
+    # you can use aliases to differentiate between them and avoid naming conflicts.
+    # alias = "Other{{ component_class_name }}"
+
+    # The props of the React component.
+    # Note: when Reflex compiles the component to Javascript,
+    # `snake_case` property names are automatically formatted as `camelCase`.
+    # The prop names may be defined in `camelCase` as well.
+    # some_prop: rx.Var[str] = "some default value"
+    # some_other_prop: rx.Var[int] = 1
+
+    # By default Reflex will install the library you have specified in the library property.
+    # However, sometimes you may need to install other libraries to use a component.
+    # In this case you can use the lib_dependencies property to specify other libraries to install.
+    # lib_dependencies: list[str] = []
+
+    # Event triggers, I did not understand the wording of the doc.
+    # def get_event_triggers(self) -> dict[str, Any]:
+    #     return {
+    #         **super().get_event_triggers(),
+    #         "on_change": lambda e0: [e0],
+    #     }
+
+    # To add custom code to your component
+    # def _get_custom_code(self) -> str:
+    #     return "const customCode = 'customCode';"
+
+
+{{ module_name }} = {{ component_class_name }}.create

+ 17 - 0
reflex/compiler/templates.py

@@ -98,3 +98,20 @@ STYLE = get_template("web/styles/styles.css.jinja2")
 
 # Code that generate the package json file
 PACKAGE_JSON = get_template("web/package.json.jinja2")
+
+# Code that generate the pyproject.toml file for custom components
+CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
+    "custom_components/pyproject.toml.jinja2"
+)
+
+# Code that generates the README file for custom components
+CUSTOM_COMPONENTS_README = get_template("custom_components/README.md.jinja2")
+
+# Code that generates the source file for custom components
+CUSTOM_COMPONENTS_SOURCE = get_template("custom_components/src.py.jinja2")
+
+# Code that generates the init file for custom components
+CUSTOM_COMPONENTS_INIT_FILE = get_template("custom_components/__init__.py.jinja2")
+
+# Code that generates the demo app main py file for testing custom components
+CUSTOM_COMPONENTS_DEMO_APP = get_template("custom_components/demo_app.py.jinja2")

+ 4 - 0
reflex/constants/__init__.py

@@ -40,6 +40,9 @@ from .config import (
     GitIgnore,
     RequirementsTxt,
 )
+from .custom_components import (
+    CustomComponents,
+)
 from .event import Endpoint, EventTriggers, SocketEvent
 from .installer import (
     Bun,
@@ -67,6 +70,7 @@ __ALL__ = [
     Config,
     COOKIES,
     ComponentName,
+    CustomComponents,
     DefaultPage,
     Dirs,
     Endpoint,

+ 30 - 0
reflex/constants/custom_components.py

@@ -0,0 +1,30 @@
+"""Constants for the custom components."""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+
+class CustomComponents(SimpleNamespace):
+    """Constants for the custom components."""
+
+    # The name of the custom components source directory.
+    SRC_DIR = "custom_components"
+    # The name of the custom components pyproject.toml file.
+    PYPROJECT_TOML = "pyproject.toml"
+    # The name of the custom components package README file.
+    PACKAGE_README = "README.md"
+    # The name of the custom components package .gitignore file.
+    PACKAGE_GITIGNORE = ".gitignore"
+    # The name of the distribution directory as result of a build.
+    DIST_DIR = "dist"
+    # The name of the init file.
+    INIT_FILE = "__init__.py"
+    # Suffixes for the distribution files.
+    DISTRIBUTION_FILE_SUFFIXES = [".tar.gz", ".whl"]
+    # The name to the URL of python package repositories.
+    REPO_URLS = {
+        # Note: the trailing slash is required for below URLs.
+        "pypi": "https://upload.pypi.org/legacy/",
+        "testpypi": "https://test.pypi.org/legacy/",
+    }

+ 1 - 0
reflex/custom_components/__init__.py

@@ -0,0 +1 @@
+"""The Reflex custom components."""

+ 565 - 0
reflex/custom_components/custom_components.py

@@ -0,0 +1,565 @@
+"""CLI for creating custom components."""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+from collections import namedtuple
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Optional
+
+import typer
+
+from reflex import constants
+from reflex.config import get_config
+from reflex.constants import CustomComponents
+from reflex.utils import console
+
+config = get_config()
+custom_components_cli = typer.Typer()
+
+
+@contextmanager
+def set_directory(working_directory: str):
+    """Context manager that sets the working directory.
+
+    Args:
+        working_directory: The working directory to change to.
+
+    Yields:
+        Yield to the caller to perform operations in the working directory.
+    """
+    current_directory = os.getcwd()
+    try:
+        os.chdir(working_directory)
+        yield
+    finally:
+        os.chdir(current_directory)
+
+
+def _create_package_config(module_name: str, package_name: str):
+    """Create a package config pyproject.toml file.
+
+    Args:
+        module_name: The name of the module.
+        package_name: The name of the package typically constructed with `reflex-` prefix and a meaningful library name.
+    """
+    from reflex.compiler import templates
+
+    with open(CustomComponents.PYPROJECT_TOML, "w") as f:
+        f.write(
+            templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render(
+                module_name=module_name, package_name=package_name
+            )
+        )
+
+
+def _create_readme(module_name: str, package_name: str):
+    """Create a package README file.
+
+    Args:
+        module_name: The name of the module.
+        package_name: The name of the python package to be published.
+    """
+    from reflex.compiler import templates
+
+    with open(CustomComponents.PACKAGE_README, "w") as f:
+        f.write(
+            templates.CUSTOM_COMPONENTS_README.render(
+                module_name=module_name,
+                package_name=package_name,
+            )
+        )
+
+
+def _write_source_and_init_py(
+    custom_component_src_dir: str,
+    component_class_name: str,
+    module_name: str,
+):
+    """Write the source code and init file from templates for the custom component.
+
+    Args:
+        custom_component_src_dir: The name of the custom component source directory.
+        component_class_name: The name of the component class.
+        module_name: The name of the module.
+    """
+    from reflex.compiler import templates
+
+    with open(
+        os.path.join(
+            custom_component_src_dir,
+            f"{module_name}.py",
+        ),
+        "w",
+    ) as f:
+        f.write(
+            templates.CUSTOM_COMPONENTS_SOURCE.render(
+                component_class_name=component_class_name, module_name=module_name
+            )
+        )
+
+    with open(
+        os.path.join(
+            custom_component_src_dir,
+            CustomComponents.INIT_FILE,
+        ),
+        "w",
+    ) as f:
+        f.write(templates.CUSTOM_COMPONENTS_INIT_FILE.render(module_name=module_name))
+
+
+def _populate_demo_app(name_variants: NameVariants):
+    """Populate the demo app that imports the custom components.
+
+    Args:
+        name_variants: the tuple including various names such as package name, class name needed for the project.
+    """
+    from reflex import constants
+    from reflex.compiler import templates
+    from reflex.reflex import _init
+
+    demo_app_dir = name_variants.demo_app_dir
+    demo_app_name = name_variants.demo_app_name
+
+    console.info(f"Creating app for testing: {demo_app_dir}")
+
+    os.makedirs(demo_app_dir)
+
+    with set_directory(demo_app_dir):
+        # We start with the blank template as basis.
+        _init(name=demo_app_name, template=constants.Templates.Kind.BLANK)
+        # Then overwrite the app source file with the one we want for testing custom components.
+        # This source file is rendered using jinja template file.
+        with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f:
+            f.write(
+                templates.CUSTOM_COMPONENTS_DEMO_APP.render(
+                    custom_component_module_dir=name_variants.custom_component_module_dir,
+                    module_name=name_variants.module_name,
+                )
+            )
+
+
+def _get_default_library_name_parts() -> list[str]:
+    """Get the default library name. Based on the current directory name, remove any non-alphanumeric characters.
+
+    Raises:
+        ValueError: If the current directory name is not suitable for python projects, and we cannot find a valid library name based off it.
+
+    Returns:
+        The parts of default library name.
+    """
+    current_dir_name = os.getcwd().split(os.path.sep)[-1]
+
+    cleaned_dir_name = re.sub("[^0-9a-zA-Z-_]+", "", current_dir_name)
+    parts = re.split("-|_", cleaned_dir_name)
+    if not parts:
+        # The folder likely has a name not suitable for python paths.
+        raise ValueError(
+            f"Could not find a valid library name based on the current directory: got {current_dir_name}."
+        )
+    return parts
+
+
+NameVariants = namedtuple(
+    "NameVariants",
+    [
+        "library_name",
+        "component_class_name",
+        "package_name",
+        "module_name",
+        "custom_component_module_dir",
+        "demo_app_dir",
+        "demo_app_name",
+    ],
+)
+
+
+def _validate_library_name(library_name: str | None) -> NameVariants:
+    """Validate the library name.
+
+    Args:
+        library_name: The name of the library if picked otherwise None.
+
+    Raises:
+        Exit: If the library name is not suitable for python projects.
+
+    Returns:
+        A tuple containing the various names such as package name, class name, etc., needed for the project.
+    """
+    if library_name is not None and not re.match(
+        r"^[a-zA-Z-]+[a-zA-Z0-9-]*$", library_name
+    ):
+        console.error(
+            f"Please use only alphanumeric characters or dashes: got {library_name}"
+        )
+        raise typer.Exit(code=1)
+
+    # If not specified, use the current directory name to form the module name.
+    name_parts = (
+        [part.lower() for part in library_name.split("-")]
+        if library_name
+        else _get_default_library_name_parts()
+    )
+    if not library_name:
+        library_name = "-".join(name_parts)
+
+    # Component class name is the camel case.
+    component_class_name = "".join([part.capitalize() for part in name_parts])
+    console.info(f"Component class name: {component_class_name}")
+
+    # Package name is commonly kebab case.
+    package_name = f"reflex-{library_name}"
+    console.info(f"Package name: {package_name}")
+
+    # Module name is the snake case.
+    module_name = "_".join(name_parts)
+
+    custom_component_module_dir = f"reflex_{module_name}"
+    console.info(f"Custom component source directory: {custom_component_module_dir}")
+
+    # Use the same name for the directory and the app.
+    demo_app_dir = demo_app_name = f"{module_name}_demo"
+    console.info(f"Demo app directory: {demo_app_dir}")
+
+    return NameVariants(
+        library_name=library_name,
+        component_class_name=component_class_name,
+        package_name=package_name,
+        module_name=module_name,
+        custom_component_module_dir=custom_component_module_dir,
+        demo_app_dir=demo_app_dir,
+        demo_app_name=demo_app_name,
+    )
+
+
+def _populate_custom_component_project(name_variants: NameVariants):
+    """Populate the custom component source directory. This includes the pyproject.toml, README.md, and the code template for the custom component.
+
+    Args:
+        name_variants: the tuple including various names such as package name, class name needed for the project.
+    """
+    console.info(
+        f"Populating pyproject.toml with package name: {name_variants.package_name}"
+    )
+    # write pyproject.toml, README.md, etc.
+    _create_package_config(
+        module_name=name_variants.library_name, package_name=name_variants.package_name
+    )
+    _create_readme(
+        module_name=name_variants.library_name, package_name=name_variants.package_name
+    )
+
+    console.info(
+        f"Initializing the component directory: {CustomComponents.SRC_DIR}/{name_variants.custom_component_module_dir}"
+    )
+    os.makedirs(CustomComponents.SRC_DIR)
+    with set_directory(CustomComponents.SRC_DIR):
+        os.makedirs(name_variants.custom_component_module_dir)
+        _write_source_and_init_py(
+            custom_component_src_dir=name_variants.custom_component_module_dir,
+            component_class_name=name_variants.component_class_name,
+            module_name=name_variants.module_name,
+        )
+
+
+@custom_components_cli.command(name="init")
+def init(
+    library_name: Optional[str] = typer.Option(
+        None,
+        help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
+    ),
+    install: bool = typer.Option(
+        True,
+        help="Whether to install package from this local custom component in editable mode.",
+    ),
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+):
+    """Initialize a custom component.
+
+    Args:
+        library_name: The name of the library.
+        install: Whether to install package from this local custom component in editable mode.
+        loglevel: The log level to use.
+
+    Raises:
+        Exit: If the pyproject.toml already exists.
+    """
+    from reflex.utils import exec, prerequisites
+
+    console.set_log_level(loglevel)
+
+    if os.path.exists(CustomComponents.PYPROJECT_TOML):
+        console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
+        typer.Exit(code=1)
+
+    # Show system info.
+    exec.output_system_info()
+
+    # Check the name follows the convention if picked.
+    name_variants = _validate_library_name(library_name)
+
+    _populate_custom_component_project(name_variants)
+
+    _populate_demo_app(name_variants)
+
+    # Initialize the .gitignore.
+    prerequisites.initialize_gitignore()
+
+    if install:
+        package_name = name_variants.package_name
+        console.info(f"Installing {package_name} in editable mode.")
+        if _pip_install_on_demand(package_name=".", install_args=["-e"]):
+            console.info(f"Package {package_name} installed!")
+        else:
+            raise typer.Exit(code=1)
+
+    console.print("Custom component initialized successfully!")
+    console.print("Here's the summary:")
+    console.print(
+        f"{CustomComponents.PYPROJECT_TOML} and {CustomComponents.PACKAGE_README} created. [bold]Please fill in details such as your name, email, homepage URL.[/bold]"
+    )
+    console.print(
+        f"Source code template is in {CustomComponents.SRC_DIR}. [bold]Start by editing it with your component implementation.[/bold]"
+    )
+    console.print(
+        f"Demo app created in {name_variants.demo_app_dir}. [bold]Use this app to test your custom component.[/bold]"
+    )
+
+
+def _pip_install_on_demand(
+    package_name: str,
+    install_args: list[str] | None = None,
+) -> bool:
+    """Install a package on demand.
+
+    Args:
+        package_name: The name of the package.
+        install_args: The additional arguments for the pip install command.
+
+    Returns:
+        True if the package is installed successfully, False otherwise.
+    """
+    install_args = install_args or []
+
+    install_cmds = [
+        sys.executable,
+        "-m",
+        "pip",
+        "install",
+        *install_args,
+        package_name,
+    ]
+    console.debug(f"Install package: {' '.join(install_cmds)}")
+    return _run_commands_in_subprocess(install_cmds)
+
+
+def _run_commands_in_subprocess(cmds: list[str]) -> bool:
+    """Run commands in a subprocess.
+
+    Args:
+        cmds: The commands to run.
+
+    Returns:
+        True if the command runs successfully, False otherwise.
+    """
+    console.debug(f"Running command: {' '.join(cmds)}")
+    try:
+        result = subprocess.run(cmds, capture_output=True, text=True, check=True)
+        console.debug(result.stdout)
+        return True
+    except subprocess.CalledProcessError as cpe:
+        console.error(cpe.stdout)
+        console.error(cpe.stderr)
+        return False
+
+
+@custom_components_cli.command(name="build")
+def build(
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+):
+    """Build a custom component. Must be run from the project root directory where the pyproject.toml is.
+
+    Args:
+        loglevel: The log level to use.
+
+    Raises:
+        Exit: If the build fails.
+    """
+    console.set_log_level(loglevel)
+    console.print("Building custom component...")
+
+    cmds = [sys.executable, "-m", "build", "."]
+    if _run_commands_in_subprocess(cmds):
+        console.info("Custom component built successfully!")
+    else:
+        raise typer.Exit(code=1)
+
+
+def _validate_repository_name(repository: str | None) -> str:
+    """Validate the repository name.
+
+    Args:
+        repository: The name of the repository.
+
+    Returns:
+        The name of the repository.
+
+    Raises:
+        Exit: If the repository name is not supported.
+    """
+    if repository is None:
+        return "pypi"
+    elif repository not in CustomComponents.REPO_URLS:
+        console.error(
+            f"Unsupported repository name. Allow {CustomComponents.REPO_URLS.keys()}, got {repository}"
+        )
+        raise typer.Exit(code=1)
+    return repository
+
+
+def _validate_credentials(
+    username: str | None, password: str | None, token: str | None
+) -> tuple[str, str]:
+    """Validate the credentials.
+
+    Args:
+        username: The username to use for authentication on python package repository.
+        password: The password to use for authentication on python package repository.
+        token: The token to use for authentication on python package repository.
+
+    Raises:
+        Exit: If the appropriate combination of credentials is not provided.
+
+    Returns:
+        The username and password.
+    """
+    if token is not None:
+        if username is not None or password is not None:
+            console.error("Cannot use token and username/password at the same time.")
+            raise typer.Exit(code=1)
+        username = "__token__"
+        password = token
+    elif username is None or password is None:
+        console.error(
+            "Must provide both username and password for authentication if not using a token."
+        )
+        raise typer.Exit(code=1)
+
+    return username, password
+
+
+def _ensure_dist_dir():
+    """Ensure the distribution directory and the expected files exist.
+
+    Raises:
+        Exit: If the distribution directory does not exist or the expected files are not found.
+    """
+    dist_dir = Path(CustomComponents.DIST_DIR)
+
+    # Check if the distribution directory exists.
+    if not dist_dir.exists():
+        console.error(f"Directory {dist_dir.name} does not exist. Please build first.")
+        raise typer.Exit(code=1)
+
+    # Check if the distribution directory is indeed a directory.
+    if not dist_dir.is_dir():
+        console.error(
+            f"{dist_dir.name} is not a directory. If this is a file you added, move it and rebuild."
+        )
+        raise typer.Exit(code=1)
+
+    # Check if the distribution files exist.
+    for suffix in CustomComponents.DISTRIBUTION_FILE_SUFFIXES:
+        if not list(dist_dir.glob(f"*{suffix}")):
+            console.error(
+                f"Expected distribution file with suffix {suffix} in directory {dist_dir.name}"
+            )
+            raise typer.Exit(code=1)
+
+
+@custom_components_cli.command(name="publish")
+def publish(
+    repository: Optional[str] = typer.Option(
+        None,
+        "-r",
+        "--repository",
+        help="The name of the repository. Defaults to pypi. Only supports pypi and testpypi (Test PyPI) for now.",
+    ),
+    token: Optional[str] = typer.Option(
+        None,
+        "-t",
+        "--token",
+        help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time",
+    ),
+    username: Optional[str] = typer.Option(
+        None,
+        "-u",
+        "--username",
+        help="The username to use for authentication on python package repository. Username and password must both be provided.",
+    ),
+    password: Optional[str] = typer.Option(
+        None,
+        "-p",
+        "--password",
+        help="The password to use for authentication on python package repository. Username and password must both be provided.",
+    ),
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+):
+    """Publish a custom component. Must be run from the project root directory where the pyproject.toml is.
+
+    Args:
+        repository: The name of the Python package repository, such pypi, testpypi.
+        token: The token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time.
+        username: The username to use for authentication on python package repository.
+        password: The password to use for authentication on python package repository.
+        loglevel: The log level to use.
+
+    Raises:
+        Exit: If arguments provided are not correct or the publish fails.
+    """
+    console.set_log_level(loglevel)
+
+    # Validate the repository name.
+    repository = _validate_repository_name(repository)
+    console.print(f"Publishing custom component to {repository}...")
+
+    # Validate the credentials.
+    username, password = _validate_credentials(username, password, token)
+
+    # Validate the distribution directory.
+    _ensure_dist_dir()
+
+    # We install twine on the fly if required so it is not a stable dependency of reflex.
+    try:
+        import twine  # noqa: F401  # type: ignore
+    except (ImportError, ModuleNotFoundError) as ex:
+        if not _pip_install_on_demand("twine"):
+            raise typer.Exit(code=1) from ex
+    publish_cmds = [
+        sys.executable,
+        "-m",
+        "twine",
+        "upload",
+        "--repository-url",
+        CustomComponents.REPO_URLS[repository],
+        "--username",
+        username,
+        "--password",
+        password,
+        "--non-interactive",
+        f"{CustomComponents.DIST_DIR}/*",
+    ]
+    if _run_commands_in_subprocess(publish_cmds):
+        console.info("Custom component published successfully!")
+    else:
+        raise typer.Exit(1)

+ 6 - 0
reflex/reflex.py

@@ -15,6 +15,7 @@ from reflex_cli.utils import dependency
 
 from reflex import constants
 from reflex.config import get_config
+from reflex.custom_components.custom_components import custom_components_cli
 from reflex.utils import console, telemetry
 
 # Disable typer+rich integration for help panels
@@ -580,6 +581,11 @@ cli.add_typer(
     name="deployments",
     help="Subcommands for managing the Deployments.",
 )
+cli.add_typer(
+    custom_components_cli,
+    name="component",
+    help="Subcommands for creating and publishing Custom Components.",
+)
 
 if __name__ == "__main__":
     cli()