diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f237e91d..7e988149 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,3 @@ -*By submitting this pull request you agree that all contributions to this project are made under the MIT license.* - ## Description A summary of the changes. @@ -8,6 +6,9 @@ A summary of the changes. Please update this checklist as you complete each item: -- [ ] Tests have been included for all bug fixes or added functionality. -- [ ] The changelog has been updated with any significant changes, if necessary. -- [ ] GitHub Issues which may be closed by this PR have been linked. +- [ ] Tests have been developed for bug fixes or new functionality. +- [ ] The changelog has been updated, if necessary. +- [ ] Documentation has been updated, if necessary. +- [ ] GitHub Issues closed by this PR have been linked. + +By submitting this pull request you agree that all contributions comply with this project's open source license(s). diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c95928..f8f0ec63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,18 @@ Using the following categories, list your changes in this order: +## [Unreleased] + +### Added + +- ReactPy components can now use SEO compatible rendering! + - `settings.py:REACTPY_PRERENDER` can be set to `True` to enable this behavior by default + - Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}` + +### Changed + +- Renamed undocumented utility function `reactpy_django.utils.ComponentPreloader` to `reactpy_django.utils.RootComponentFinder`. + ## [3.5.1] - 2023-09-07 ### Added diff --git a/docs/python/template-tag-bad-view.py b/docs/python/template-tag-bad-view.py index a798abb0..00d0d9f7 100644 --- a/docs/python/template-tag-bad-view.py +++ b/docs/python/template-tag-bad-view.py @@ -2,5 +2,5 @@ def example_view(request): - context_vars = {"dont_do_this": "example_project.my_app.components.hello_world"} + context_vars = {"my_variable": "example_project.my_app.components.hello_world"} return render(request, "my-template.html", context_vars) diff --git a/docs/src/about/code.md b/docs/src/about/code.md index b163d01c..b4790d5b 100644 --- a/docs/src/about/code.md +++ b/docs/src/about/code.md @@ -35,7 +35,7 @@ Then, by running the command below you can: - Download, build, and install Javascript dependencies ```bash linenums="0" -pip install -e . -r requirements.txt +pip install -e . -r requirements.txt --verbose --upgrade ``` !!! warning "Pitfall" diff --git a/docs/src/about/docs.md b/docs/src/about/docs.md index 6c2f413b..5dbd35dd 100644 --- a/docs/src/about/docs.md +++ b/docs/src/about/docs.md @@ -28,7 +28,7 @@ Then, by running the command below you can: - Self-host a test server for the documentation ```bash linenums="0" -pip install -e . -r requirements.txt --upgrade +pip install -r requirements.txt --upgrade ``` Finally, to verify that everything is working properly, you can manually run the docs preview web server. diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css index f71fa55a..7813830c 100644 --- a/docs/src/assets/css/admonition.css +++ b/docs/src/assets/css/admonition.css @@ -1,20 +1,20 @@ [data-md-color-scheme="slate"] { --admonition-border-color: transparent; --admonition-expanded-border-color: rgba(255, 255, 255, 0.1); - --note-bg-color: rgb(43 110 98/ 0.2); + --note-bg-color: rgba(43, 110, 98, 0.2); --terminal-bg-color: #0c0c0c; --terminal-title-bg-color: #000; - --deep-dive-bg-color: rgb(43 52 145 / 0.2); + --deep-dive-bg-color: rgba(43, 52, 145, 0.2); --you-will-learn-bg-color: #353a45; - --pitfall-bg-color: rgb(182 87 0 / 0.2); + --pitfall-bg-color: rgba(182, 87, 0, 0.2); } [data-md-color-scheme="default"] { --admonition-border-color: rgba(0, 0, 0, 0.08); --admonition-expanded-border-color: var(--admonition-border-color); - --note-bg-color: rgb(244 251 249); - --terminal-bg-color: rgb(64 71 86); - --terminal-title-bg-color: rgb(35 39 47); - --deep-dive-bg-color: rgb(243 244 253); + --note-bg-color: rgb(244, 251, 249); + --terminal-bg-color: rgb(64, 71, 86); + --terminal-title-bg-color: rgb(35, 39, 47); + --deep-dive-bg-color: rgb(243, 244, 253); --you-will-learn-bg-color: rgb(246, 247, 249); --pitfall-bg-color: rgb(254, 245, 231); } @@ -81,12 +81,12 @@ React Name: "Note" font-size: 1rem; background: transparent; padding-bottom: 0; - color: rgb(68 172 153); + color: rgb(68, 172, 153); } .md-typeset .note .admonition-title:before { font-size: 1.1rem; - background: rgb(68 172 153); + background: rgb(68, 172, 153); } .md-typeset .note > .admonition-title:before, @@ -109,12 +109,12 @@ React Name: "Pitfall" font-size: 1rem; background: transparent; padding-bottom: 0; - color: rgb(219 125 39); + color: rgb(219, 125, 39); } .md-typeset .warning .admonition-title:before { font-size: 1.1rem; - background: rgb(219 125 39); + background: rgb(219, 125, 39); } /* @@ -131,12 +131,12 @@ React Name: "Deep Dive" font-size: 1rem; background: transparent; padding-bottom: 0; - color: rgb(136 145 236); + color: rgb(136, 145, 236); } .md-typeset .info .admonition-title:before { font-size: 1.1rem; - background: rgb(136 145 236); + background: rgb(136, 145, 236); } /* @@ -152,11 +152,11 @@ React Name: "Terminal" .md-typeset .example .admonition-title { background: var(--terminal-title-bg-color); - color: rgb(246 247 249); + color: rgb(246, 247, 249); } .md-typeset .example .admonition-title:before { - background: rgb(246 247 249); + background: rgb(246, 247, 249); } .md-typeset .admonition.example code { diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css index d1556dc0..c5465498 100644 --- a/docs/src/assets/css/code.css +++ b/docs/src/assets/css/code.css @@ -9,7 +9,7 @@ --md-code-hl-color: #ffffcf1c; --md-code-bg-color: #16181d; --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); - --code-tab-color: rgb(52 58 70); + --code-tab-color: rgb(52, 58, 70); --md-code-hl-name-color: #aadafc; --md-code-hl-string-color: hsl(21 49% 63% / 1); --md-code-hl-keyword-color: hsl(289.67deg 35% 60%); diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index 500ae4be..da5a74c4 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -3,7 +3,7 @@ --reactpy-color: #58b962; --reactpy-color-dark: #42914a; --reactpy-color-darker: #34743b; - --reactpy-color-opacity-10: rgb(88 185 98 / 10%); + --reactpy-color-opacity-10: rgba(88, 185, 98, 0.1); } [data-md-color-accent="red"] { @@ -12,7 +12,7 @@ } [data-md-color-scheme="slate"] { - --md-default-bg-color: rgb(35 39 47); + --md-default-bg-color: rgb(35, 39, 47); --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54); --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26); --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07); diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css index 4f0db7fa..33e8b14f 100644 --- a/docs/src/assets/css/navbar.css +++ b/docs/src/assets/css/navbar.css @@ -1,9 +1,11 @@ [data-md-color-scheme="slate"] { --md-header-border-color: rgb(255 255 255 / 5%); + --md-version-bg-color: #ffffff0d; } [data-md-color-scheme="default"] { --md-header-border-color: rgb(0 0 0 / 7%); + --md-version-bg-color: #ae58ee2e; } .md-header { @@ -28,12 +30,20 @@ } .md-version__list { - margin: 0.2rem -0.8rem; + margin: 0; + left: 0; + right: 0; + top: 2.5rem; } -[dir="ltr"] .md-header__title.md-header__title--active { - margin: 0; - transition: margin 0.35s ease; +.md-version { + background: var(--md-version-bg-color); + border-radius: 999px; + padding: 0 0.8rem; + margin: 0.3rem 0; + height: 1.8rem; + display: flex; + font-size: 0.7rem; } /* Mobile Styling */ @@ -97,6 +107,12 @@ .md-header__topic { position: relative; } + .md-header__title--active .md-header__topic { + transform: none; + opacity: 1; + pointer-events: auto; + z-index: 4; + } /* Search */ .md-search { diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css index aeadf3b5..bf197138 100644 --- a/docs/src/assets/css/sidebar.css +++ b/docs/src/assets/css/sidebar.css @@ -28,7 +28,7 @@ } .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - color: rgb(133 142 159); + color: rgb(133, 142, 159); margin: 0.5rem; } diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md index 7d7c949f..311bc3c3 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -53,6 +53,8 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject ??? note "Configure ReactPy settings (Optional)" + {% include "../reference/settings.md" start="" end="" %} + {% include "../reference/settings.md" start="" end="" %} ## Step 3: Configure `urls.py` diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index 9f108a0d..3a013ed7 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -2,7 +2,11 @@

-Your **Django project's** `settings.py` can modify the behavior of ReactPy. + + +These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. + +

@@ -14,25 +18,34 @@ Your **Django project's** `settings.py` can modify the behavior of ReactPy. --- -## Primary Configuration - -These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. +## General Settings | Setting | Default Value | Example Value(s) | Description | | --- | --- | --- | --- | -| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | -| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` | -| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `#!python args` and `#!python kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. | -| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy WebSocket and HTTP URLs. | -| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. | +| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix used for all ReactPy WebSocket and HTTP URLs. | +| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.postprocessor"`, `#!python None` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the function must contain a `#!python data` parameter. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. | | `#!python REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `#!python AuthMiddlewareStack` and...
2. You are using Django's `#!python AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `#!python backend` attribute. | -| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). | -| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. | -| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. | + +## Performance Settings + +| Setting | Default Value | Example Value(s) | Description | +| --- | --- | --- | --- | +| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Multiprocessing-safe database used to store ReactPy session data. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` | +| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used for ReactPy JavaScript modules. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | +| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Configures whether ReactPy components are rendered in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). | +| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir"]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. | +| `#!python REACTPY_PRERENDER` | `#!python False` | `#!python True` | Configures whether to pre-render your components, which enables SEO compatibility and increases perceived responsiveness. You can use the `#!python prerender` argument in your [template tag](../reference/template-tag.md#component) as a manual override. During pre-rendering, there are some key differences in behavior:
1. Only the component's first render is pre-rendered.
2. All `#!python connection` related hooks use HTTP.
3. `#!python html.script` is executed during both pre-render and render.
4. Component is non-interactive until a WebSocket connection is formed. | + +## Stability Settings + +| Setting | Default Value | Example Value(s) | Description | +| --- | --- | --- | --- | +| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. | +| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this value to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. | | `#!python REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. | | `#!python REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. | -| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. | +| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy component sessions. This includes data such as `#!python *args` and `#!python **kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. | diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 6f20be36..b9b9017f 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -25,8 +25,9 @@ This template tag can be used to insert any number of ReactPy components onto yo | `#!python dotted_path` | `#!python str` | The dotted path to the component to render. | N/A | | `#!python *args` | `#!python Any` | The positional arguments to provide to the component. | N/A | | `#!python class` | `#!python str | None` | The HTML class to apply to the top-level component div. | `#!python None` | - | `#!python key` | `#!python str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` | - | `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If set to `#!python None`, the host will be automatically configured.
Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` | + | `#!python key` | `#!python Any` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` | + | `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If unset, the host will be automatically configured.
Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` | + | `#!python prerender` | `#!python str` | If `#!python "True"`, the component will pre-rendered, which enables SEO compatibility and increases perceived responsiveness. | `#!python "False"` | | `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A | **Returns** @@ -37,11 +38,11 @@ This template tag can be used to insert any number of ReactPy components onto yo -??? warning "Do not use context variables for the ReactPy component name" +??? warning "Do not use context variables for the component path" - Our preprocessor relies on the template tag containing a string. + The ReactPy component finder (`#!python reactpy_django.utils.RootComponentFinder`) requires that your component path is a string. - **Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior. + **Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior, such as components that will not render. For example, **do not** do the following: @@ -52,7 +53,7 @@ This template tag can be used to insert any number of ReactPy components onto yo {% component "example_project.my_app.components.hello_world" recipient="World" %} - {% component dont_do_this recipient="World" %} + {% component my_variable recipient="World" %} ``` === "views.py" @@ -81,7 +82,7 @@ This template tag can be used to insert any number of ReactPy components onto yo 1. If your host address are completely separate ( `origin1.com != origin2.com` ) you will need to [configure CORS headers](https://pypi.org/project/django-cors-headers/) on your main application during deployment. 2. You will not need to register ReactPy HTTP or WebSocket paths on any applications that do not perform any component rendering. - 3. Your component will only be able to access `#!python *args`/`#!python **kwargs` you provide to the template tag if your applications share a common database. + 3. Your component will only be able to access your template tag's `#!python *args`/`#!python **kwargs` if your applications share a common database. diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index d71facc2..e5c10057 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -52,9 +52,9 @@ This function is used manually register a root component with ReactPy. {% include "../../python/register-component.py" %} ``` -??? warning "Only use this within `#!python AppConfig.ready()`" +??? warning "Only use this within `#!python MyAppConfig.ready()`" - You should always call `#!python register_component` within a Django [`#!python AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. + You should always call `#!python register_component` within a Django [`#!python MyAppConfig.ready()` method](https://docs.djangoproject.com/en/dev/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. ??? question "Do I need to use this?" diff --git a/mkdocs.yml b/mkdocs.yml index e4e19c65..9269b109 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -112,7 +112,7 @@ watch: site_name: ReactPy-Django site_author: Archmonger -site_description: It's React, but in Python. Now for Django developers. +site_description: It's React, but in Python. Now with Django integration. copyright: Copyright © 2023 Reactive Python. repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django diff --git a/setup.py b/setup.py index 9fecde36..9174e81a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from logging import StreamHandler, getLogger from pathlib import Path -from setuptools import find_packages, setup +from setuptools import find_namespace_packages, setup from setuptools.command.develop import develop from setuptools.command.sdist import sdist @@ -47,27 +47,35 @@ def list2cmdline(cmd_list): package = { "name": name, "python_requires": ">=3.9", - "packages": find_packages(str(src_dir)), + "packages": find_namespace_packages(str(src_dir)), "package_dir": {"": "src"}, - "description": "Control the web with Python", - "author": "Ryan Morshead", - "author_email": "ryan.morshead@gmail.com", + "description": "It's React, but in Python. Now with Django integration.", + "author": "Mark Bakhit", + "author_email": "archiethemonger@gmail.com", "url": "https://github.com/reactive-python/reactpy-django", "license": "MIT", "platforms": "Linux, Mac OS X, Windows", - "keywords": ["interactive", "widgets", "DOM", "React"], + "keywords": [ + "interactive", + "reactive", + "widgets", + "DOM", + "React", + "ReactJS", + "ReactPy", + ], "include_package_data": True, "zip_safe": False, "classifiers": [ "Framework :: Django", "Framework :: Django :: 4.0", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Multimedia :: Graphics", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Environment :: Web Environment", ], } @@ -129,9 +137,8 @@ def run(self): log.info(f"> {list2cmdline(args_list)}") subprocess.run(args_list, cwd=js_dir, check=True) except Exception: - log.error("Failed to update NPM") log.error(traceback.format_exc()) - raise + log.error("Failed to update NPM, continuing anyway...") log.info("Installing Javascript...") try: @@ -139,8 +146,8 @@ def run(self): log.info(f"> {list2cmdline(args_list)}") subprocess.run(args_list, cwd=js_dir, check=True) except Exception: - log.error("Failed to install Javascript") log.error(traceback.format_exc()) + log.error("Failed to install Javascript") raise log.info("Building Javascript...") @@ -149,8 +156,8 @@ def run(self): log.info(f"> {list2cmdline(args_list)}") subprocess.run(args_list, cwd=js_dir, check=True) except Exception: - log.error("Failed to build Javascript") log.error(traceback.format_exc()) + log.error("Failed to build Javascript") raise log.info("Successfully built Javascript") diff --git a/src/js/package-lock.json b/src/js/package-lock.json index 84321d16..f4ffe4f8 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -13,8 +13,11 @@ "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.2", + "@types/react": "^17.0", + "@types/react-dom": "^17.0", "prettier": "^3.0.2", - "rollup": "^3.28.1" + "rollup": "^3.28.1", + "typescript": "^4.9.5" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -158,12 +161,44 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "17.0.65", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.65.tgz", + "integrity": "sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz", + "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==", + "dev": true, + "dependencies": { + "@types/react": "^17" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -197,6 +232,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, "node_modules/deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", @@ -389,7 +430,7 @@ "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "peer": true, "engines": { "node": ">=0.10.0" @@ -521,16 +562,15 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "peer": true, + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/wrappy": { @@ -618,12 +658,44 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "17.0.65", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.65.tgz", + "integrity": "sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz", + "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==", + "dev": true, + "requires": { + "@types/react": "^17" + } + }, "@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -651,6 +723,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", @@ -806,7 +884,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "peer": true }, "once": { @@ -895,10 +973,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "peer": true + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" }, "wrappy": { "version": "1.0.2", diff --git a/src/js/package.json b/src/js/package.json index 40596a0d..0c61ec46 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -13,6 +13,9 @@ "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.2", + "@types/react": "^17.0", + "@types/react-dom": "^17.0", + "typescript": "^4.9.5", "prettier": "^3.0.2", "rollup": "^3.28.1" }, diff --git a/src/js/src/index.ts b/src/js/src/index.ts index 53d67c6f..56a85aac 100644 --- a/src/js/src/index.ts +++ b/src/js/src/index.ts @@ -1,4 +1,4 @@ -import { mount } from "@reactpy/client"; +import { mount } from "./mount"; import { ReactPyDjangoClient } from "./client"; export function mountComponent( diff --git a/src/js/src/mount.tsx b/src/js/src/mount.tsx new file mode 100644 index 00000000..4d7cdcb3 --- /dev/null +++ b/src/js/src/mount.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "react-dom"; +import { Layout } from "@reactpy/client/src/components"; +import { ReactPyDjangoClient } from "./client"; + +export function mount(element: HTMLElement, client: ReactPyDjangoClient): void { + const prerenderElement = document.getElementById(element.id + "-prerender"); + if (prerenderElement) { + element.hidden = true; + client.onMessage("layout-update", ({ path, model }) => { + if (prerenderElement) { + prerenderElement.replaceWith(element); + element.hidden = false; + } + }); + } + render(, element); +} diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 7da4aa77..7e5ec6cb 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -3,5 +3,14 @@ "target": "ES2017", "module": "esnext", "moduleResolution": "node", + "jsx": "react", }, + "paths": { + "react": [ + "./node_modules/preact/compat/" + ], + "react-dom": [ + "./node_modules/preact/compat/" + ] + } } diff --git a/src/reactpy_django/apps.py b/src/reactpy_django/apps.py index 67a39cb2..f0f2c455 100644 --- a/src/reactpy_django/apps.py +++ b/src/reactpy_django/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -from reactpy_django.utils import ComponentPreloader +from reactpy_django.utils import RootComponentFinder class ReactPyConfig(AppConfig): @@ -8,4 +8,4 @@ class ReactPyConfig(AppConfig): def ready(self): # Populate the ReactPy component registry when Django is ready - ComponentPreloader().run() + RootComponentFinder().run() diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index 0025008b..489888eb 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -473,4 +473,13 @@ def reactpy_errors(app_configs, **kwargs): ) ) + if not isinstance(config.REACTPY_PRERENDER, bool): + errors.append( + Error( + "Invalid type for REACTPY_PRERENDER.", + hint="REACTPY_PRERENDER should be a boolean.", + id="reactpy_django.E021", + ) + ) + return errors diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index dc350e2a..56011c83 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -107,3 +107,8 @@ "REACTPY_RECONNECT_BACKOFF_MULTIPLIER", 1.25, # Default to 25% backoff per connection attempt ) +REACTPY_PRERENDER: bool = getattr( + settings, + "REACTPY_PRERENDER", + False, +) diff --git a/src/reactpy_django/exceptions.py b/src/reactpy_django/exceptions.py index 5cdcb719..49c6fef3 100644 --- a/src/reactpy_django/exceptions.py +++ b/src/reactpy_django/exceptions.py @@ -8,3 +8,7 @@ class ComponentDoesNotExistError(AttributeError): class InvalidHostError(ValueError): ... + + +class ComponentCarrierError(ValueError): + ... diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index c60bbf1c..8115de56 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -48,16 +48,27 @@ def use_origin() -> str | None: this will be None.""" scope = _use_scope() try: - return next( - ( - header[1].decode("utf-8") - for header in scope["headers"] - if header[0] == b"origin" - ), - None, - ) + if scope["type"] == "websocket": + return next( + ( + header[1].decode("utf-8") + for header in scope["headers"] + if header[0] == b"origin" + ), + None, + ) + if scope["type"] == "http": + host = next( + ( + header[1].decode("utf-8") + for header in scope["headers"] + if header[0] == b"host" + ) + ) + return f"{scope['scheme']}://{host}" if host else None except Exception: - return None + _logger.info("Failed to get origin") + return None def use_scope() -> dict[str, Any]: diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html index 75a65dbe..1a6b4669 100644 --- a/src/reactpy_django/templates/reactpy/component.html +++ b/src/reactpy_django/templates/reactpy/component.html @@ -1,9 +1,11 @@ {% load static %} -{% if reactpy_failure %} -{% if reactpy_debug_mode %} + +{% if reactpy_failure and reactpy_debug_mode %} {% firstof reactpy_error "UnknownError" %}: "{% firstof reactpy_dotted_path "UnknownPath" %}" {% endif %} -{% else %} + +{% if not reactpy_failure %} +{% if reactpy_prerender_html %}
{{ reactpy_prerender_html|safe }}
{% endif %}