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 %}