浏览代码

[Fix 477] Use jinja2 for templating (#915)

PeterYusuke 2 年之前
父节点
当前提交
3b88e7c329

+ 83 - 5
poetry.lock

@@ -463,6 +463,84 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+    {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
+]
+
 [[package]]
 name = "mypy-extensions"
 version = "1.0.0"
@@ -1063,18 +1141,18 @@ files = [
 
 [[package]]
 name = "redis"
-version = "4.5.4"
+version = "4.5.5"
 description = "Python client for Redis database and key-value store"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "redis-4.5.4-py3-none-any.whl", hash = "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2"},
-    {file = "redis-4.5.4.tar.gz", hash = "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893"},
+    {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"},
+    {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"},
 ]
 
 [package.dependencies]
-async-timeout = {version = ">=4.0.2", markers = "python_version <= \"3.11.2\""}
+async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""}
 importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""}
 typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 
@@ -1600,4 +1678,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "8c9f764a830657316f774fb679895c89d6874460582f4dbf8b2edbd4f534b262"
+content-hash = "43e53d9fff649b6a939ec953411b8932e512600b5af3edc0cbbbbb6c5576168b"

+ 19 - 17
pynecone/compiler/compiler.py

@@ -1,7 +1,6 @@
 """Compiler for the pynecone apps."""
 from __future__ import annotations
 
-import json
 from functools import wraps
 from typing import Callable, List, Set, Tuple, Type
 
@@ -10,7 +9,7 @@ from pynecone.compiler import templates, utils
 from pynecone.components.component import Component, CustomComponent
 from pynecone.state import State
 from pynecone.style import Style
-from pynecone.utils import imports, path_ops
+from pynecone.utils import imports
 from pynecone.var import ImportVar
 
 # Imports to be included in every Pynecone app.
@@ -42,7 +41,7 @@ def _compile_document_root(root: Component) -> str:
     Returns:
         The compiled document root.
     """
-    return templates.DOCUMENT_ROOT(
+    return templates.DOCUMENT_ROOT.render(
         imports=utils.compile_imports(root.get_imports()),
         document=root.render(),
     )
@@ -57,7 +56,7 @@ def _compile_theme(theme: dict) -> str:
     Returns:
         The compiled theme.
     """
-    return templates.THEME(theme=json.dumps(theme))
+    return templates.THEME.render(theme=theme)
 
 
 def _compile_page(component: Component, state: Type[State]) -> str:
@@ -72,17 +71,20 @@ def _compile_page(component: Component, state: Type[State]) -> str:
     """
     # Merge the default imports with the app-specific imports.
     imports = utils.merge_imports(DEFAULT_IMPORTS, component.get_imports())
+    imports = utils.compile_imports(imports)
 
     # Compile the code to render the component.
-    return templates.PAGE(
-        imports=utils.compile_imports(imports),
-        custom_code=path_ops.join(component.get_custom_code()),
-        constants=utils.compile_constants(),
-        state=utils.compile_state(state),
-        events=utils.compile_events(state),
-        effects=utils.compile_effects(state),
-        hooks=path_ops.join(component.get_hooks()),
+    return templates.PAGE.render(
+        imports=imports,
+        custom_codes=component.get_custom_code(),
+        endpoints={
+            constant.name: constant.get_url() for constant in constants.Endpoint
+        },
+        initial_state=utils.compile_state(state),
+        state_name=state.get_name(),
+        hooks=component.get_hooks(),
         render=component.render(),
+        transports=constants.Transports.POLLING_WEBSOCKET.get_transports(),
     )
 
 
@@ -99,18 +101,18 @@ def _compile_components(components: Set[CustomComponent]) -> str:
         "react": {ImportVar(tag="memo")},
         f"/{constants.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")},
     }
-    component_defs = []
+    component_renders = []
 
     # Compile each component.
     for component in components:
-        component_def, component_imports = utils.compile_custom_component(component)
-        component_defs.append(component_def)
+        component_render, component_imports = utils.compile_custom_component(component)
+        component_renders.append(component_render)
         imports = utils.merge_imports(imports, component_imports)
 
     # Compile the components page.
-    return templates.COMPONENTS(
+    return templates.COMPONENTS.render(
         imports=utils.compile_imports(imports),
-        components=path_ops.join(component_defs),
+        components=component_renders,
     )
 
 

+ 51 - 188
pynecone/compiler/templates.py

@@ -1,163 +1,75 @@
 """Templates to use in the pynecone compiler."""
 
-from typing import Optional, Set
+from jinja2 import Environment, FileSystemLoader, Template
 
 from pynecone import constants
 from pynecone.utils import path_ops
-
-# Template for the Pynecone config file.
-PCCONFIG = f"""import pynecone as pc
-
-class {{config_name}}(pc.Config):
-    pass
-
-config = {{config_name}}(
-    app_name="{{app_name}}",
-    db_url="{constants.DB_URL}",
-    env=pc.Env.DEV,
-)
-"""
-
-# Javascript formatting.
-CONST = "const {name} = {value}".format
-PROP = "{object}.{property}".format
-IMPORT_LIB = 'import "{lib}"'.format
-IMPORT_FIELDS = 'import {default}{others} from "{lib}"'.format
-
-
-def format_import(lib: str, default: str = "", rest: Optional[Set[str]] = None) -> str:
-    """Format an import statement.
+from pynecone.utils.format import json_dumps
+
+
+class PyneconeJinjaEnvironment(Environment):
+    """The template class for jinja environment."""
+
+    def __init__(self) -> None:
+        """Set default environment."""
+        extensions = ["jinja2.ext.debug"]
+        super().__init__(
+            extensions=extensions,
+            trim_blocks=True,
+            lstrip_blocks=True,
+        )
+        self.filters["json_dumps"] = json_dumps
+        self.filters["react_setter"] = lambda state: f"set{state.capitalize()}"
+        self.loader = FileSystemLoader(constants.JINJA_TEMPLATE_DIR)
+        self.globals["const"] = {
+            "socket": constants.SOCKET,
+            "result": constants.RESULT,
+            "router": constants.ROUTER,
+            "event_endpoint": constants.Endpoint.EVENT.name,
+            "events": constants.EVENTS,
+            "state": constants.STATE,
+            "processing": constants.PROCESSING,
+            "initial_result": {
+                constants.STATE: None,
+                constants.EVENTS: [],
+                constants.PROCESSING: False,
+            },
+            "color_mode": constants.COLOR_MODE,
+            "toggle_color_mode": constants.TOGGLE_COLOR_MODE,
+            "use_color_mode": constants.USE_COLOR_MODE,
+        }
+
+
+def get_template(name: str) -> Template:
+    """Get render function that work with a template.
 
     Args:
-        lib: The library to import from.
-        default: The default field to import.
-        rest: The set of fields to import from the library.
+        name: The template name. "/" is used as the path separator.
 
     Returns:
-        The compiled import statement.
+        A render function.
     """
-    # Handle the case of direct imports with no libraries.
-    if not lib:
-        assert not default, "No default field allowed for empty library."
-        assert rest is not None and len(rest) > 0, "No fields to import."
-        return path_ops.join([IMPORT_LIB(lib=lib) for lib in sorted(rest)])
-
-    # Handle importing from a library.
-    rest = rest or set()
-    if len(default) == 0 and len(rest) == 0:
-        # Handle the case of importing a library with no fields.
-        return IMPORT_LIB(lib=lib)
-    # Handle importing specific fields from a library.
-    others = f'{{{", ".join(sorted(rest))}}}' if len(rest) > 0 else ""
-    if default != "" and len(rest) > 0:
-        default += ", "
-    return IMPORT_FIELDS(default=default, others=others, lib=lib)
+    return PyneconeJinjaEnvironment().get_template(name=name)
 
 
+# Template for the Pynecone config file.
+PCCONFIG = get_template("app/pcconfig.py.jinja2")
+
 # Code to render a NextJS Document root.
-DOCUMENT_ROOT = path_ops.join(
-    [
-        "{imports}",
-        "export default function Document() {{",
-        "return (",
-        "{document}",
-        ")",
-        "}}",
-    ]
-).format
+DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2")
 
 # Template for the theme file.
-THEME = "export default {theme}".format
+THEME = get_template("web/utils/theme.js.jinja2")
 
 # Code to render a single NextJS page.
-PAGE = path_ops.join(
-    [
-        "{imports}",
-        "{custom_code}",
-        "{constants}",
-        "export default function Component() {{",
-        "{state}",
-        "{events}",
-        "{effects}",
-        "{hooks}",
-        "return (",
-        "{render}",
-        ")",
-        "}}",
-    ]
-).format
-
-# Code to render a single exported custom component.
-COMPONENT = path_ops.join(
-    [
-        "export const {name} = memo(({{{props}}}) => (",
-        "{render}",
-        "))",
-    ]
-).format
+PAGE = get_template("web/pages/index.js.jinja2")
 
 # Code to render the custom components page.
-COMPONENTS = path_ops.join(
-    [
-        "{imports}",
-        "{components}",
-    ]
-).format
-
-
-# React state declarations.
-USE_STATE = CONST(
-    name="[{state}, {set_state}]", value="useState({initial_state})"
-).format
-
-
-def format_state_setter(state: str) -> str:
-    """Format a state setter.
-
-    Args:
-        state: The name of the state variable.
-
-    Returns:
-        The compiled state setter.
-    """
-    return f"set{state[0].upper() + state[1:]}"
-
-
-def format_state(
-    state: str,
-    initial_state: str,
-) -> str:
-    """Format a state declaration.
-
-    Args:
-        state: The name of the state variable.
-        initial_state: The initial state of the state variable.
-
-    Returns:
-        The compiled state declaration.
-    """
-    set_state = format_state_setter(state)
-    return USE_STATE(state=state, set_state=set_state, initial_state=initial_state)
+COMPONENTS = get_template("web/pages/custom_component.js.jinja2")
 
+# Sitemap config file.
+SITEMAP_CONFIG = "module.exports = {config}".format
 
-# Events.
-EVENT_ENDPOINT = constants.Endpoint.EVENT.name
-EVENT_FN = path_ops.join(
-    [
-        "const Event = events => {set_state}({{",
-        "  ...{state},",
-        "  events: [...{state}.events, ...events],",
-        "}})",
-    ]
-).format
-UPLOAD_FN = path_ops.join(
-    [
-        "const File = files => {set_state}({{",
-        "  ...{state},",
-        "  files,",
-        "}})",
-    ]
-).format
 FULL_CONTROL = path_ops.join(
     [
         "{{setState(prev => ({{",
@@ -167,52 +79,3 @@ FULL_CONTROL = path_ops.join(
         ")}}",
     ]
 ).format
-
-# Effects.
-ROUTER = constants.ROUTER
-RESULT = constants.RESULT
-PROCESSING = constants.PROCESSING
-SOCKET = constants.SOCKET
-STATE = constants.STATE
-EVENTS = constants.EVENTS
-SET_RESULT = format_state_setter(RESULT)
-READY = f"const {{ isReady }} = {ROUTER};"
-USE_EFFECT = path_ops.join(
-    [
-        "useEffect(() => {{",
-        "  if(!isReady) {{",
-        "    return;",
-        "  }}",
-        f"  if (!{SOCKET}.current) {{{{",
-        f"    connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT}, {{transports}})",
-        "  }}",
-        "  const update = async () => {{",
-        f"    if ({RESULT}.{STATE} != null) {{{{",
-        f"      {{set_state}}({{{{",
-        f"        ...{RESULT}.{STATE},",
-        f"        events: [...{{state}}.{EVENTS}, ...{RESULT}.{EVENTS}],",
-        "      }})",
-        f"      {SET_RESULT}({{{{",
-        f"        {STATE}: null,",
-        f"        {EVENTS}: [],",
-        f"        {PROCESSING}: false,",
-        "      }})",
-        "    }}",
-        f"    await updateState({{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {SOCKET}.current)",
-        "  }}",
-        "  update()",
-        "}})",
-    ]
-).format
-
-# Routing
-ROUTER = f"const {constants.ROUTER} = useRouter()"
-
-# Sockets.
-SOCKET = "const socket = useRef(null)"
-
-# Color toggle
-COLORTOGGLE = f"const {{ {constants.COLOR_MODE}, {constants.TOGGLE_COLOR_MODE} }} = {constants.USE_COLOR_MODE}()"
-
-# Sitemap config file.
-SITEMAP_CONFIG = "module.exports = {config}".format

+ 41 - 106
pynecone/compiler/utils.py

@@ -1,11 +1,9 @@
 """Common utility functions used in the compiler."""
 
-import json
 import os
 from typing import Dict, List, Optional, Set, Tuple, Type
 
 from pynecone import constants
-from pynecone.compiler import templates
 from pynecone.components.base import (
     Body,
     ColorModeScript,
@@ -31,15 +29,16 @@ from pynecone.var import ImportVar
 merge_imports = imports.merge_imports
 
 
-def compile_import_statement(lib: str, fields: Set[ImportVar]) -> str:
+def compile_import_statement(fields: Set[ImportVar]) -> Tuple[str, Set[str]]:
     """Compile an import statement.
 
     Args:
-        lib: The library to import from.
         fields: The set of fields to import from the library.
 
     Returns:
-        The compiled import statement.
+        The libraries for default and rest.
+        default: default library. When install "import def from library".
+        rest: rest of libraries. When install "import {rest1, rest2} from library"
     """
     # Check for default imports.
     defaults = {field for field in fields if field.is_default}
@@ -48,58 +47,59 @@ def compile_import_statement(lib: str, fields: Set[ImportVar]) -> str:
     # Get the default import, and the specific imports.
     default = next(iter({field.name for field in defaults}), "")
     rest = {field.name for field in fields - defaults}
-    return templates.format_import(lib=lib, default=default, rest=rest)
 
+    return default, rest
 
-def compile_imports(imports: imports.ImportDict) -> str:
+
+def compile_imports(imports: imports.ImportDict) -> List[dict]:
     """Compile an import dict.
 
     Args:
         imports: The import dict to compile.
 
     Returns:
-        The compiled import dict.
+        The list of import dict.
     """
-    return path_ops.join(
-        [compile_import_statement(lib, fields) for lib, fields in imports.items()]
-    )
-
+    import_dicts = []
+    for lib, fields in imports.items():
+        default, rest = compile_import_statement(fields)
+        if not lib:
+            assert not default, "No default field allowed for empty library."
+            assert rest is not None and len(rest) > 0, "No fields to import."
+            for module in sorted(rest):
+                import_dicts.append(get_import_dict(module))
+            continue
 
-def compile_constant_declaration(name: str, value: str) -> str:
-    """Compile a constant declaration.
-
-    Args:
-        name: The name of the constant.
-        value: The value of the constant.
+        import_dicts.append(get_import_dict(lib, default, rest))
+    return import_dicts
 
-    Returns:
-        The compiled constant declaration.
-    """
-    return templates.CONST(name=name, value=json.dumps(value))
 
+def get_import_dict(lib: str, default: str = "", rest: Optional[Set] = None) -> Dict:
+    """Get dictionary for import template.
 
-def compile_constants() -> str:
-    """Compile all the necessary constants.
+    Args:
+        lib: The importing react library.
+        default: The default module to import.
+        rest: The rest module to import.
 
     Returns:
-        A string of all the compiled constants.
+        A dictionary for import template.
     """
-    return path_ops.join(
-        [
-            compile_constant_declaration(name=endpoint.name, value=endpoint.get_url())
-            for endpoint in constants.Endpoint
-        ]
-    )
+    return {
+        "lib": lib,
+        "default": default,
+        "rest": rest if rest else set(),
+    }
 
 
-def compile_state(state: Type[State]) -> str:
+def compile_state(state: Type[State]) -> Dict:
     """Compile the state of the app.
 
     Args:
         state: The app state object.
 
     Returns:
-        A string of the compiled state.
+        A dictionary of the compiled state.
     """
     initial_state = state().dict()
     initial_state.update(
@@ -108,77 +108,12 @@ def compile_state(state: Type[State]) -> str:
             "files": [],
         }
     )
-    initial_state = format.format_state(initial_state)
-    synced_state = templates.format_state(
-        state=state.get_name(), initial_state=json.dumps(initial_state)
-    )
-    initial_result = {
-        constants.STATE: None,
-        constants.EVENTS: [],
-        constants.PROCESSING: False,
-    }
-    result = templates.format_state(
-        state="result",
-        initial_state=json.dumps(initial_result),
-    )
-    router = templates.ROUTER
-    socket = templates.SOCKET
-    ready = templates.READY
-    color_toggle = templates.COLORTOGGLE
-    return path_ops.join([synced_state, result, router, socket, ready, color_toggle])
-
-
-def compile_events(state: Type[State]) -> str:
-    """Compile all the events for a given component.
-
-    Args:
-        state: The state class for the component.
-
-    Returns:
-        A string of the compiled events for the component.
-    """
-    state_name = state.get_name()
-    state_setter = templates.format_state_setter(state_name)
-    return path_ops.join(
-        [
-            templates.EVENT_FN(state=state_name, set_state=state_setter),
-            templates.UPLOAD_FN(state=state_name, set_state=state_setter),
-        ]
-    )
-
-
-def compile_effects(state: Type[State]) -> str:
-    """Compile all the effects for a given component.
-
-    Args:
-        state: The state class for the component.
-
-    Returns:
-        A string of the compiled effects for the component.
-    """
-    state_name = state.get_name()
-    set_state = templates.format_state_setter(state_name)
-    transports = constants.Transports.POLLING_WEBSOCKET.get_transports()
-    return templates.USE_EFFECT(
-        state=state_name, set_state=set_state, transports=transports
-    )
-
-
-def compile_render(component: Component) -> str:
-    """Compile the component's render method.
-
-    Args:
-        component: The component to compile the render method for.
-
-    Returns:
-        A string of the compiled render method.
-    """
-    return component.render()
+    return format.format_state(initial_state)
 
 
 def compile_custom_component(
     component: CustomComponent,
-) -> Tuple[str, imports.ImportDict]:
+) -> Tuple[dict, imports.ImportDict]:
     """Compile a custom component.
 
     Args:
@@ -198,15 +133,15 @@ def compile_custom_component(
     }
 
     # Concatenate the props.
-    props = ", ".join([prop.name for prop in component.get_prop_vars()])
+    props = [prop.name for prop in component.get_prop_vars()]
 
     # Compile the component.
     return (
-        templates.COMPONENT(
-            name=component.tag,
-            props=props,
-            render=render,
-        ),
+        {
+            "name": component.tag,
+            "props": props,
+            "render": render.render(),
+        },
         imports,
     )
 

+ 3 - 4
pynecone/components/base/meta.py

@@ -1,6 +1,6 @@
 """Display the title of the current page."""
 
-from typing import Optional
+from typing import Dict, Optional
 
 from pynecone.components.base.bare import Bare
 from pynecone.components.component import Component
@@ -11,18 +11,17 @@ class Title(Component):
 
     tag = "title"
 
-    def render(self) -> str:
+    def render(self) -> Dict:
         """Render the title component.
 
         Returns:
             The rendered title component.
         """
-        tag = self._render()
         # Make sure the title is a single string.
         assert len(self.children) == 1 and isinstance(
             self.children[0], Bare
         ), "Title must be a single string."
-        return str(tag.set(contents=str(self.children[0].contents)))
+        return super().render()
 
 
 class Meta(Component):

+ 10 - 10
pynecone/components/component.py

@@ -21,7 +21,7 @@ from pynecone.event import (
     get_handler_args,
 )
 from pynecone.style import Style
-from pynecone.utils import format, imports, path_ops, types
+from pynecone.utils import format, imports, types
 from pynecone.var import BaseVar, ImportVar, Var
 
 
@@ -289,7 +289,7 @@ class Component(Base, ABC):
         Returns:
             The code to render the component.
         """
-        return self.render()
+        return format.json_dumps(self.render())
 
     def __str__(self) -> str:
         """Represent the component in React.
@@ -297,7 +297,7 @@ class Component(Base, ABC):
         Returns:
             The code to render the component.
         """
-        return self.render()
+        return format.json_dumps(self.render())
 
     def _render(self) -> Tag:
         """Define how to render the component in React.
@@ -393,14 +393,14 @@ class Component(Base, ABC):
             child.add_style(style)
         return self
 
-    def render(self) -> str:
+    def render(self) -> Dict:
         """Render the component.
 
         Returns:
-            The code to render the component.
+            The dictionary for template of component.
         """
         tag = self._render()
-        return str(
+        return dict(
             tag.add_props(
                 **self.event_triggers,
                 key=self.key,
@@ -408,10 +408,10 @@ class Component(Base, ABC):
                 id=self.id,
                 class_name=self.class_name,
             ).set(
-                contents=path_ops.join(
-                    [str(tag.contents)] + [child.render() for child in self.children]
-                ).strip(),
-            )
+                children=[child.render() for child in self.children],
+                contents=str(tag.contents),
+                props=tag.format_props(),
+            ),
         )
 
     def _get_custom_code(self) -> Optional[str]:

+ 26 - 4
pynecone/components/layout/cond.py

@@ -1,7 +1,7 @@
 """Create a list of components from an iterable."""
 from __future__ import annotations
 
-from typing import Any, Optional
+from typing import Any, Dict, Optional
 
 from pynecone.components.component import Component
 from pynecone.components.layout.fragment import Fragment
@@ -24,7 +24,7 @@ class Cond(Component):
 
     @classmethod
     def create(
-        cls, cond: Var, comp1: Component, comp2: Optional[Component] = None
+        cls, cond: Var, comp1: Component, comp2: Optional[Component]
     ) -> Component:
         """Create a conditional component.
 
@@ -37,8 +37,10 @@ class Cond(Component):
             The conditional component.
         """
         # Wrap everything in fragments.
-        comp1 = Fragment.create(comp1)
-        comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
+        if comp1.__class__.__name__ != "Fragment":
+            comp1 = Fragment.create(comp1)
+        if comp2 is None or comp2.__class__.__name__ != "Fragment":
+            comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
         return Fragment.create(
             cls(
                 cond=cond,
@@ -55,6 +57,26 @@ class Cond(Component):
             false_value=self.comp2.render(),
         )
 
+    def render(self) -> Dict:
+        """Render the component.
+
+        Returns:
+            The dictionary for template of component.
+        """
+        tag = self._render()
+        return dict(
+            tag.add_props(
+                **self.event_triggers,
+                key=self.key,
+                sx=self.style,
+                id=self.id,
+                class_name=self.class_name,
+            ).set(
+                props=tag.format_props(),
+            ),
+            cond_state=f"isTrue({self.cond.full_name})",
+        )
+
 
 def cond(condition: Any, c1: Any, c2: Any = None):
     """Create a conditional component or Prop.

+ 36 - 3
pynecone/components/layout/foreach.py

@@ -4,8 +4,8 @@ from __future__ import annotations
 from typing import Any, Callable, List
 
 from pynecone.components.component import Component
-from pynecone.components.tags import IterTag, Tag
-from pynecone.var import BaseVar, Var
+from pynecone.components.tags import IterTag
+from pynecone.var import BaseVar, Var, get_unique_variable_name
 
 
 class Foreach(Component):
@@ -49,5 +49,38 @@ class Foreach(Component):
             **props,
         )
 
-    def _render(self) -> Tag:
+    def _render(self) -> IterTag:
         return IterTag(iterable=self.iterable, render_fn=self.render_fn)
+
+    def render(self):
+        """Render the component.
+
+        Returns:
+            The dictionary for template of component.
+        """
+        tag = self._render()
+        try:
+            type_ = self.iterable.type_.__args__[0]
+        except Exception:
+            type_ = Any
+        arg = BaseVar(
+            name=get_unique_variable_name(),
+            type_=type_,
+        )
+        index_arg = tag.get_index_var_arg()
+        component = tag.render_component(self.render_fn, arg)
+        return dict(
+            tag.add_props(
+                **self.event_triggers,
+                key=self.key,
+                sx=self.style,
+                id=self.id,
+                class_name=self.class_name,
+            ).set(
+                children=[component.render()],
+                props=tag.format_props(),
+            ),
+            iterable_state=tag.iterable.full_name,
+            arg_name=arg.name,
+            arg_index=index_arg,
+        )

+ 3 - 17
pynecone/components/tags/cond_tag.py

@@ -1,9 +1,8 @@
 """Tag to conditionally render components."""
 
-from typing import Any
+from typing import Any, Dict, Optional
 
 from pynecone.components.tags.tag import Tag
-from pynecone.utils import format
 from pynecone.var import Var
 
 
@@ -14,20 +13,7 @@ class CondTag(Tag):
     cond: Var[Any]
 
     # The code to render if the condition is true.
-    true_value: str
+    true_value: Dict
 
     # The code to render if the condition is false.
-    false_value: str
-
-    def __str__(self) -> str:
-        """Render the tag as a React string.
-
-        Returns:
-            The React code to render the tag.
-        """
-        assert self.cond is not None, "The condition must be set."
-        return format.format_cond(
-            cond=self.cond.full_name,
-            true_value=self.true_value,
-            false_value=self.false_value,
-        )
+    false_value: Optional[Dict]

+ 2 - 24
pynecone/components/tags/iter_tag.py

@@ -2,11 +2,10 @@
 from __future__ import annotations
 
 import inspect
-from typing import TYPE_CHECKING, Any, Callable, List
+from typing import TYPE_CHECKING, Callable, List
 
 from pynecone.components.tags.tag import Tag
-from pynecone.utils import format
-from pynecone.var import BaseVar, Var, get_unique_variable_name
+from pynecone.var import Var
 
 if TYPE_CHECKING:
     from pynecone.components.component import Component
@@ -83,24 +82,3 @@ class IterTag(Tag):
             component.key = index
 
         return component
-
-    def __str__(self) -> str:
-        """Render the tag as a React string.
-
-        Returns:
-            The React code to render the tag.
-        """
-        try:
-            type_ = self.iterable.type_.__args__[0]
-        except Exception:
-            type_ = Any
-        arg = BaseVar(
-            name=get_unique_variable_name(),
-            type_=type_,
-        )
-        index_arg = self.get_index_var_arg()
-        component = self.render_component(self.render_fn, arg)
-        return format.wrap(
-            f"{self.iterable.full_name}.map(({arg.name}, {index_arg}) => {component})",
-            "{",
-        )

+ 9 - 40
pynecone/components/tags/tag.py

@@ -3,9 +3,8 @@
 from __future__ import annotations
 
 import json
-import os
 import re
-from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
 
 from plotly.graph_objects import Figure
 from plotly.io import to_json
@@ -37,6 +36,9 @@ class Tag(Base):
     # Special props that aren't key value pairs.
     special_props: Set[Var] = set()
 
+    # The children components.
+    children: List[Any] = []
+
     def __init__(self, *args, **kwargs):
         """Initialize the tag.
 
@@ -117,55 +119,22 @@ class Tag(Base):
         assert isinstance(prop, str), "The prop must be a string."
         return format.wrap(prop, "{", check_first=False)
 
-    def format_props(self) -> str:
+    def format_props(self) -> List:
         """Format the tag's props.
 
         Returns:
-            The formatted props.
+            The formatted props list.
         """
         # If there are no props, return an empty string.
         if len(self.props) == 0:
-            return ""
+            return []
 
         # Format all the props.
-        return os.linesep.join(
+        return [
             f"{name}={self.format_prop(prop)}"
             for name, prop in sorted(self.props.items())
             if prop is not None
-        )
-
-    def __str__(self) -> str:
-        """Render the tag as a React string.
-
-        Returns:
-            The React code to render the tag.
-        """
-        # Get the tag props.
-        props_str = self.format_props()
-
-        # Add the special props.
-        props_str += " ".join([str(prop) for prop in self.special_props])
-
-        # Add a space if there are props.
-        if len(props_str) > 0:
-            props_str = " " + props_str
-
-        if len(self.contents) == 0:
-            # If there is no inner content, we don't need a closing tag.
-            tag_str = format.wrap(f"{self.name}{props_str}/", "<")
-        else:
-            if self.args is not None:
-                # If there are args, wrap the tag in a function call.
-                args_str = ", ".join(self.args)
-                contents = f"{{({{{args_str}}}) => ({self.contents})}}"
-            else:
-                contents = self.contents
-            # Otherwise wrap it in opening and closing tags.
-            open = format.wrap(f"{self.name}{props_str}", "<")
-            close = format.wrap(f"/{self.name}", "<")
-            tag_str = format.wrap(contents, open, close)
-
-        return tag_str
+        ] + [str(prop) for prop in self.special_props]
 
     def add_props(self, **kwargs: Optional[Any]) -> Tag:
         """Add props to the tag.

+ 2 - 0
pynecone/constants.py

@@ -33,6 +33,8 @@ TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, ".templates")
 WEB_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "web")
 # The assets subdirectory of the template directory.
 ASSETS_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, APP_ASSETS_DIR)
+# The jinja template directory.
+JINJA_TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, "templates")
 
 # The frontend directories in a project.
 # The web folder where the NextJS app is compiled to.

+ 6 - 6
pynecone/el/element.py

@@ -1,7 +1,8 @@
 """Base class definition for raw HTML elements."""
 
+from typing import Dict
+
 from pynecone.components.component import Component
-from pynecone.utils import path_ops
 
 
 class Element(Component):
@@ -12,14 +13,14 @@ class Element(Component):
     prop.
     """
 
-    def render(self) -> str:
+    def render(self) -> Dict:
         """Render the element.
 
         Returns:
             The code to render the element.
         """
         tag = self._render()
-        return str(
+        return dict(
             tag.add_props(
                 **self.event_triggers,
                 key=self.key,
@@ -27,9 +28,8 @@ class Element(Component):
                 style=self.style,
                 class_name=self.class_name,
             ).set(
-                contents=path_ops.join(
-                    [str(tag.contents)] + [child.render() for child in self.children]
-                ).strip(),
+                contents=str(tag.contents),
+                children=[child.render() for child in self.children],
             )
         )
 

+ 10 - 0
pynecone/templates/app/pcconfig.py.jinja2

@@ -0,0 +1,10 @@
+import pynecone as pc
+
+class {{ config_name }}(pc.Config):
+    pass
+
+config = {{ config_name }}(
+    app_name="{{ app_name }}",
+    db_url="{{ db_url }}",
+    env=pc.Env.DEV,
+)

+ 9 - 0
pynecone/templates/web/pages/_document.js.jinja2

@@ -0,0 +1,9 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block export %}
+export default function Document() {
+  return (
+    {{utils.render(document, indent_width=4)}}
+  )
+}
+{% endblock %}

+ 13 - 0
pynecone/templates/web/pages/base_page.js.jinja2

@@ -0,0 +1,13 @@
+{% import 'web/pages/utils.js.jinja2' as utils %}
+
+{%- block imports_libs %}
+{% for module in imports%}
+  {{- utils.get_import(module) }}
+{% endfor %}
+{% endblock %}
+
+{% block declaration %}
+{% endblock %}
+
+{% block export %}
+{% endblock %}

+ 10 - 0
pynecone/templates/web/pages/custom_component.js.jinja2

@@ -0,0 +1,10 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block export %}
+{% for component in components %}
+
+export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => (
+  {{utils.render(component.render)}}
+))
+{% endfor %}
+{% endblock %}

+ 66 - 0
pynecone/templates/web/pages/index.js.jinja2

@@ -0,0 +1,66 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block declaration %}
+{% for custom_code in custom_codes %}
+{{custom_code}}
+{% endfor %}
+
+{% for name, url in endpoints.items() %}
+const {{name}} = {{url|json_dumps}}
+{% endfor %}
+{% endblock %}
+
+{% block export %}
+export default function Component() {
+  const [{{state_name}}, {{state_name|react_setter}}] = useState({{initial_state|json_dumps}})
+  const [{{const.result}}, {{const.result|react_setter}}] = useState({{const.initial_result|json_dumps}})
+  const {{const.router}} = useRouter()
+  const {{const.socket}} = useRef(null)
+  const { isReady } = {{const.router}}
+  const { {{const.color_mode}}, {{const.toggle_color_mode}} } = {{const.use_color_mode}}()
+
+  const Event = events => {{state_name|react_setter}}({
+    ...{{state_name}},
+    events: [...{{state_name}}.events, ...events],
+  })
+
+  const File = files => {{state_name|react_setter}}({
+    ...{{state_name}},
+    files,
+  })
+
+  useEffect(()=>{
+    if(!isReady) {
+      return;
+    }
+    if (!{{const.socket}}.current) {
+      connect({{const.socket}}, {{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{const.event_endpoint}}, {{transports}})
+    }
+    const update = async () => {
+      if ({{const.result}}.{{const.state}} != null){
+        {{state_name|react_setter}}({
+          ...{{const.result}}.{{const.state}},
+          events: [...{{state_name}}.{{const.events}}, ...{{const.result}}.{{const.events}}],
+        })
+
+        {{const.result|react_setter}}({
+          {{const.state}}: null,
+          {{const.events}}: [],
+          {{const.processing}}: false,
+        })
+      }
+
+      await updateState({{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{const.socket}}.current)
+      }
+      update()
+  })
+
+  {% for hook in hooks %}
+  {{ hook }}
+  {% endfor %}
+
+  return (
+    {{utils.render(render, indent_width=4)}}
+  )
+}
+{% endblock %}

+ 110 - 0
pynecone/templates/web/pages/utils.js.jinja2

@@ -0,0 +1,110 @@
+{# Renderting components recursively. #}
+{# Args: #}
+{#     component: component dictionary #}
+{#     indent_width: indent width #}
+{% macro render(component, indent_width=2) %}
+{% filter indent(width=indent_width) %}
+  {%- if component is not mapping %}
+    {{- component }}
+  {%- elif component.iterable %}
+    {{- render_iterable_tag(component) }}
+  {%- elif component.cond %}
+    {{- render_condition_tag(component) }}
+  {%- elif component.children|length %}
+    {{- render_tag(component) }}
+  {%- else %}
+    {{- render_self_close_tag(component) }}
+  {%- endif %}
+{% endfilter %}
+{% endmacro %}
+
+{# Renderting self close tag. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_self_close_tag(component) %}
+{%- if component.name|length %}
+<{{ component.name }} {{- render_props(component.props) }}/>
+{%- else %}
+  {{- component.contents }}
+{%- endif %}
+{% endmacro %}
+
+{# Renderting close tag with args and props. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_tag(component) %}
+<{{component.name}} {{- render_props(component.props) }}>
+{%- if component.args is not none -%}
+  {{- render_arg_content(component) }}
+{%- else -%}
+  {{ component.contents }}
+  {% for child in component.children %}
+  {{ render(child) }}
+  {% endfor %}
+{%- endif -%}
+</{{component.name}}>
+{%- endmacro %}
+
+
+{# Renderting condition component. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_condition_tag(component) %}
+{ {{- component.cond_state }} ? (
+  {{ render(component.true_value) }}
+) : (
+  {{ render(component.false_value) }}
+)}
+{%- endmacro %}
+
+
+{# Renderting iterable component. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_iterable_tag(component) %}
+{ {{- component.iterable_state }}.map(({{ component.arg_name }}, {{ component.arg_index }}) => (
+  {% for child in component.children %}
+  {{ render(child) }}
+  {% endfor %}
+))}
+{%- endmacro %}
+
+
+{# Renderting props of a component. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_props(props) %}
+{% if props|length %} {{ props|join(" ") }}{% endif %}
+{% endmacro %}
+
+
+{# Renderting content with args. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_arg_content(component) %}
+{% filter indent(width=2) %}
+{# no string below for a line break #}
+
+{({ {{component.args|join(", ")}} }) => (
+  {% for child in component.children %}
+  {{ render(child) }}
+  {% endfor %}
+)}
+{% endfilter %}
+{% endmacro %}
+
+
+{# Get react libraries import . #}
+{# Args: #}
+{#     module: react module dictionary #}
+{% macro get_import(module)%}
+{%- if module.default|length and module.rest|length -%}
+  import {{module.default}}, { {{module.rest|sort|join(", ")}} } from "{{module.lib}}"
+{%- elif module.default|length -%}
+  import {{module.default}} from "{{module.lib}}"
+{%- elif module.rest|length -%}
+  import { {{module.rest|sort|join(", ")}} } from "{{module.lib}}"
+{%- else -%}
+  import "{{module.lib}}"
+{%- endif -%}
+{% endmacro %}

+ 1 - 0
pynecone/templates/web/utils/theme.js.jinja2

@@ -0,0 +1 @@
+export default {{ theme|json_dumps }}

+ 1 - 3
pynecone/utils/format.py

@@ -306,11 +306,9 @@ def format_upload_event(event_spec: EventSpec) -> str:
     Returns:
         The compiled event.
     """
-    from pynecone.compiler import templates
-
     state, name = get_event_handler_parts(event_spec.handler)
     parent_state = state.split(".")[0]
-    return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",UPLOAD)'
+    return f'uploadFiles({parent_state}, {constants.RESULT}, set{constants.RESULT.capitalize()}, {parent_state}.files, "{state}.{name}",UPLOAD)'
 
 
 def format_full_control_event(event_chain: EventChain) -> str:

+ 1 - 1
pynecone/utils/prerequisites.py

@@ -151,7 +151,7 @@ def create_config(app_name: str):
 
     config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
     with open(constants.CONFIG_FILE, "w") as f:
-        f.write(templates.PCCONFIG.format(app_name=app_name, config_name=config_name))
+        f.write(templates.PCCONFIG.render(app_name=app_name, config_name=config_name))
 
 
 def create_web_directory(root: Path) -> str:

+ 1 - 0
pyproject.toml

@@ -40,6 +40,7 @@ websockets = "^10.4"
 cloudpickle = "^2.2.1"
 python-multipart = "^0.0.5"
 watchdog = "^2.3.1"
+jinja2 = "^3.1.2"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.2"

+ 58 - 45
tests/compiler/test_compiler.py

@@ -1,4 +1,4 @@
-from typing import Set
+from typing import List, Set
 
 import pytest
 
@@ -8,51 +8,55 @@ from pynecone.var import ImportVar
 
 
 @pytest.mark.parametrize(
-    "lib,fields,output",
+    "fields,test_default,test_rest",
     [
         (
-            "axios",
             {ImportVar(tag="axios", is_default=True)},
-            'import axios from "axios"',
+            "axios",
+            set(),
         ),
         (
-            "axios",
             {ImportVar(tag="foo"), ImportVar(tag="bar")},
-            'import {bar, foo} from "axios"',
+            "",
+            {"foo", "bar"},
         ),
         (
-            "axios",
             {
                 ImportVar(tag="axios", is_default=True),
                 ImportVar(tag="foo"),
                 ImportVar(tag="bar"),
             },
-            "import " "axios, " "{bar, " "foo} from " '"axios"',
+            "axios",
+            {"foo", "bar"},
         ),
     ],
 )
-def test_compile_import_statement(lib: str, fields: Set[ImportVar], output: str):
+def test_compile_import_statement(
+    fields: Set[ImportVar], test_default: str, test_rest: str
+):
     """Test the compile_import_statement function.
 
     Args:
-        lib: The library name.
         fields: The fields to import.
-        output: The expected output.
+        test_default: The expected output of default library.
+        test_rest: The expected output rest libraries.
     """
-    assert utils.compile_import_statement(lib, fields) == output
+    default, rest = utils.compile_import_statement(fields)
+    assert default == test_default
+    assert rest == test_rest
 
 
 @pytest.mark.parametrize(
-    "import_dict,output",
+    "import_dict,test_dicts",
     [
-        ({}, ""),
+        ({}, []),
         (
             {"axios": {ImportVar(tag="axios", is_default=True)}},
-            'import axios from "axios"',
+            [{"lib": "axios", "default": "axios", "rest": set()}],
         ),
         (
             {"axios": {ImportVar(tag="foo"), ImportVar(tag="bar")}},
-            'import {bar, foo} from "axios"',
+            [{"lib": "axios", "default": "", "rest": {"foo", "bar"}}],
         ),
         (
             {
@@ -63,52 +67,61 @@ def test_compile_import_statement(lib: str, fields: Set[ImportVar], output: str)
                 },
                 "react": {ImportVar(tag="react", is_default=True)},
             },
-            'import axios, {bar, foo} from "axios"\nimport react from "react"',
+            [
+                {"lib": "axios", "default": "axios", "rest": {"foo", "bar"}},
+                {"lib": "react", "default": "react", "rest": set()},
+            ],
         ),
         (
             {"": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")}},
-            'import "lib1.js"\nimport "lib2.js"',
+            [
+                {"lib": "lib1.js", "default": "", "rest": set()},
+                {"lib": "lib2.js", "default": "", "rest": set()},
+            ],
         ),
         (
             {
                 "": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")},
                 "axios": {ImportVar(tag="axios", is_default=True)},
             },
-            'import "lib1.js"\nimport "lib2.js"\nimport axios from "axios"',
+            [
+                {"lib": "lib1.js", "default": "", "rest": set()},
+                {"lib": "lib2.js", "default": "", "rest": set()},
+                {"lib": "axios", "default": "axios", "rest": set()},
+            ],
         ),
     ],
 )
-def test_compile_imports(
-    import_dict: imports.ImportDict, output: str, windows_platform: bool
-):
+def test_compile_imports(import_dict: imports.ImportDict, test_dicts: List[dict]):
     """Test the compile_imports function.
 
     Args:
         import_dict: The import dictionary.
-        output: The expected output.
-        windows_platform: whether system is windows.
+        test_dicts: The expected output.
     """
-    assert utils.compile_imports(import_dict) == (
-        output.replace("\n", "\r\n") if windows_platform else output
-    )
+    imports = utils.compile_imports(import_dict)
+    for import_dict, test_dict in zip(imports, test_dicts):
+        assert import_dict["lib"] == test_dict["lib"]
+        assert import_dict["default"] == test_dict["default"]
+        assert import_dict["rest"] == test_dict["rest"]
 
 
-@pytest.mark.parametrize(
-    "name,value,output",
-    [
-        ("foo", "bar", 'const foo = "bar"'),
-        ("num", 1, "const num = 1"),
-        ("check", False, "const check = false"),
-        ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
-        ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
-    ],
-)
-def test_compile_constant_declaration(name: str, value: str, output: str):
-    """Test the compile_constant_declaration function.
+# @pytest.mark.parametrize(
+#     "name,value,output",
+#     [
+#         ("foo", "bar", 'const foo = "bar"'),
+#         ("num", 1, "const num = 1"),
+#         ("check", False, "const check = false"),
+#         ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
+#         ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
+#     ],
+# )
+# def test_compile_constant_declaration(name: str, value: str, output: str):
+#     """Test the compile_constant_declaration function.
 
-    Args:
-        name: The name of the constant.
-        value: The value of the constant.
-        output: The expected output.
-    """
-    assert utils.compile_constant_declaration(name, value) == output
+#     Args:
+#         name: The name of the constant.
+#         value: The value of the constant.
+#         output: The expected output.
+#     """
+#     assert utils.compile_constant_declaration(name, value) == output

+ 2 - 2
tests/components/base/test_bare.py

@@ -19,5 +19,5 @@ def test_fstrings(contents, expected):
         contents: The contents of the component.
         expected: The expected output.
     """
-    comp = Bare.create(contents)
-    assert str(comp) == expected
+    comp = Bare.create(contents).render()
+    assert comp["contents"] == expected

+ 6 - 7
tests/components/datadisplay/test_datatable.py

@@ -1,5 +1,3 @@
-import os
-
 import pandas as pd
 import pytest
 
@@ -37,11 +35,12 @@ def test_validate_data_table(data_table_state: pc.Var, expected):
         props["columns"] = data_table_state.columns
     data_table_component = data_table(**props)
 
-    assert (
-        str(data_table_component)
-        == f"<DataTableGrid columns={{{expected}.columns}}{os.linesep}data={{"
-        f"{expected}.data}}/>"
-    )
+    data_table_dict = data_table_component.render()
+
+    assert data_table_dict["props"] == [
+        f"columns={{{expected}.columns}}",
+        f"data={{{expected}.data}}",
+    ]
 
 
 @pytest.mark.parametrize(

+ 37 - 20
tests/components/forms/test_uploads.py

@@ -1,5 +1,3 @@
-import os
-
 import pytest
 
 import pynecone as pc
@@ -49,14 +47,36 @@ def test_upload_component_render(upload_component):
     Args:
         upload_component: component fixture
     """
+    uplaod = upload_component.render()
+
+    # upload
+    assert uplaod["name"] == "ReactDropzone"
+    assert uplaod["props"] == [
+        "multiple={true}",
+        "onDrop={e => File(e)}",
+    ]
+    assert uplaod["args"] == ("getRootProps", "getInputProps")
+
+    # box inside of upload
+    [box] = uplaod["children"]
+    assert box["name"] == "Box"
+    assert box["props"] == [
+        'sx={{"border": "1px dotted black"}}',
+        "{...getRootProps()}",
+    ]
+
+    # input, button and text inside of box
+    [input, button, text] = box["children"]
+    assert input["name"] == "Input"
+    assert input["props"] == ['type="file"', "{...getInputProps()}"]
+
+    assert button["name"] == "Button"
+    assert button["children"][0]["contents"] == "{`select file`}"
+
+    assert text["name"] == "Text"
     assert (
-        str(upload_component) == f"<ReactDropzone multiple={{true}}{os.linesep}"
-        "onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box "
-        'sx={{"border": "1px dotted black"}}{...getRootProps()}><Input '
-        f'type="file"{{...getInputProps()}}/>{os.linesep}'
-        f"<Button>{{`select file`}}</Button>{os.linesep}"
-        "<Text>{`Drag and drop files here or click to select "
-        "files`}</Text></Box>)}</ReactDropzone>"
+        text["children"][0]["contents"]
+        == "{`Drag and drop files here or click to select files`}"
     )
 
 
@@ -66,14 +86,11 @@ def test_upload_component_with_props_render(upload_component_with_props):
     Args:
         upload_component_with_props: component fixture
     """
-    assert (
-        str(upload_component_with_props) == f"<ReactDropzone maxFiles={{2}}{os.linesep}"
-        f"multiple={{true}}{os.linesep}"
-        f"noDrag={{true}}{os.linesep}"
-        "onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box "
-        'sx={{"border": "1px dotted black"}}{...getRootProps()}><Input '
-        f'type="file"{{...getInputProps()}}/>{os.linesep}'
-        f"<Button>{{`select file`}}</Button>{os.linesep}"
-        "<Text>{`Drag and drop files here or click to select "
-        "files`}</Text></Box>)}</ReactDropzone>"
-    )
+    uplaod = upload_component_with_props.render()
+
+    assert uplaod["props"] == [
+        "maxFiles={2}",
+        "multiple={true}",
+        "noDrag={true}",
+        "onDrop={e => File(e)}",
+    ]

+ 20 - 5
tests/components/layout/test_cond.py

@@ -38,12 +38,27 @@ def test_validate_cond(cond_state: pc.Var):
         Text.create("cond is True"),
         Text.create("cond is False"),
     )
+    cond_dict = cond_component.render() if type(cond_component) == Fragment else {}
+    assert cond_dict["name"] == "Fragment"
 
-    assert str(cond_component) == (
-        "<Fragment>{isTrue(cond_state.value) ? "
-        "<Fragment><Text>{`cond is True`}</Text></Fragment> : "
-        "<Fragment><Text>{`cond is False`}</Text></Fragment>}</Fragment>"
-    )
+    [condition] = cond_dict["children"]
+    assert condition["cond_state"] == "isTrue(cond_state.value)"
+
+    # true value
+    true_value = condition["true_value"]
+    assert true_value["name"] == "Fragment"
+
+    [true_value_text] = true_value["children"]
+    assert true_value_text["name"] == "Text"
+    assert true_value_text["children"][0]["contents"] == "{`cond is True`}"
+
+    # false value
+    false_value = condition["false_value"]
+    assert false_value["name"] == "Fragment"
+
+    [false_value_text] = false_value["children"]
+    assert false_value_text["name"] == "Text"
+    assert false_value_text["children"][0]["contents"] == "{`cond is False`}"
 
 
 @pytest.mark.parametrize(

+ 50 - 30
tests/components/test_tag.py

@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 import pytest
 
@@ -71,25 +71,24 @@ def test_format_prop(prop: Var, formatted: str):
 
 
 @pytest.mark.parametrize(
-    "props,formatted",
+    "props,test_props",
     [
-        ({}, ""),
-        ({"key": 1}, "key={1}"),
-        ({"key": "value"}, 'key="value"'),
-        ({"key": True, "key2": "value2"}, 'key={true}\nkey2="value2"'),
+        ({}, []),
+        ({"key": 1}, ["key={1}"]),
+        ({"key": "value"}, ['key="value"']),
+        ({"key": True, "key2": "value2"}, ["key={true}", 'key2="value2"']),
     ],
 )
-def test_format_props(props: Dict[str, Var], formatted: str, windows_platform: bool):
+def test_format_props(props: Dict[str, Var], test_props: List):
     """Test that the formatted props are correct.
 
     Args:
         props: The props to test.
-        formatted: The expected formatted props.
-        windows_platform: Whether the system is windows.
+        test_props: The expected props.
     """
-    assert Tag(props=props).format_props() == (
-        formatted.replace("\n", "\r\n") if windows_platform else formatted
-    )
+    tag_props = Tag(props=props).format_props()
+    for i, tag_prop in enumerate(tag_props):
+        assert tag_prop == test_props[i]
 
 
 @pytest.mark.parametrize(
@@ -126,13 +125,20 @@ def test_add_props():
 @pytest.mark.parametrize(
     "tag,expected",
     [
-        (Tag(), "</>"),
-        (Tag(name="br"), "<br/>"),
-        (Tag(contents="hello"), "<>hello</>"),
-        (Tag(name="h1", contents="hello"), "<h1>hello</h1>"),
+        (Tag(), {"name": "", "contents": "", "props": {}}),
+        (Tag(name="br"), {"name": "br", "contents": "", "props": {}}),
+        (Tag(contents="hello"), {"name": "", "contents": "hello", "props": {}}),
+        (
+            Tag(name="h1", contents="hello"),
+            {"name": "h1", "contents": "hello", "props": {}},
+        ),
         (
             Tag(name="box", props={"color": "red", "textAlign": "center"}),
-            '<box color="red"\ntextAlign="center"/>',
+            {
+                "name": "box",
+                "contents": "",
+                "props": {"color": "red", "textAlign": "center"},
+            },
         ),
         (
             Tag(
@@ -140,30 +146,44 @@ def test_add_props():
                 props={"color": "red", "textAlign": "center"},
                 contents="text",
             ),
-            '<box color="red"\ntextAlign="center">text</box>',
+            {
+                "name": "box",
+                "contents": "text",
+                "props": {"color": "red", "textAlign": "center"},
+            },
         ),
     ],
 )
-def test_format_tag(tag: Tag, expected: str, windows_platform: bool):
-    """Test that the formatted tag is correct.
+def test_format_tag(tag: Tag, expected: Dict):
+    """Test that the tag dict is correct.
 
     Args:
         tag: The tag to test.
-        expected: The expected formatted tag.
-        windows_platform: Whether the system is windows.
+        expected: The expected tag dictionary.
     """
-    expected = expected.replace("\n", "\r\n") if windows_platform else expected
-    assert str(tag) == expected
+    tag_dict = dict(tag)
+    assert tag_dict["name"] == expected["name"]
+    assert tag_dict["contents"] == expected["contents"]
+    assert tag_dict["props"] == expected["props"]
 
 
 def test_format_cond_tag():
-    """Test that the formatted cond tag is correct."""
+    """Test that the cond tag dict is correct."""
     tag = CondTag(
-        true_value=str(Tag(name="h1", contents="True content")),
-        false_value=str(Tag(name="h2", contents="False content")),
+        true_value=dict(Tag(name="h1", contents="True content")),
+        false_value=dict(Tag(name="h2", contents="False content")),
         cond=BaseVar(name="logged_in", type_=bool),
     )
-    assert (
-        str(tag)
-        == "{isTrue(logged_in) ? <h1>True content</h1> : <h2>False content</h2>}"
+    tag_dict = dict(tag)
+    cond, true_value, false_value = (
+        tag_dict["cond"],
+        tag_dict["true_value"],
+        tag_dict["false_value"],
     )
+    assert cond == "logged_in"
+
+    assert true_value["name"] == "h1"
+    assert true_value["contents"] == "True content"
+
+    assert false_value["name"] == "h2"
+    assert false_value["contents"] == "False content"

+ 1 - 1
tests/test_utils.py

@@ -335,7 +335,7 @@ def test_create_config(app_name, expected_config_name, mocker):
     mocker.patch("builtins.open")
     tmpl_mock = mocker.patch("pynecone.compiler.templates.PCCONFIG")
     prerequisites.create_config(app_name)
-    tmpl_mock.format.assert_called_with(
+    tmpl_mock.render.assert_called_with(
         app_name=app_name, config_name=expected_config_name
     )