diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7e988149..a5553200 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,8 @@ ## Description -A summary of the changes. + -## Checklist: +## Checklist Please update this checklist as you complete each item: @@ -11,4 +11,4 @@ Please update this checklist as you complete each item: - [ ] 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). +By submitting this pull request I agree that all contributions comply with this project's open source license(s). diff --git a/CHANGELOG.md b/CHANGELOG.md index 004520c7..9cbf92a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,9 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +### Added + +- An "offline component" can now be displayed when the client disconnects from the server. ## [3.6.0] - 2024-01-10 @@ -103,9 +105,14 @@ Using the following categories, list your changes in this order: - Prettier WebSocket URLs for components that do not have sessions. - Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled. - Bumped the minimum `@reactpy/client` version to `0.3.1` -- Bumped the minimum Django version to `4.2`. - Use TypeScript instead of JavaScript for this repository. - - Note: ReactPy-Django will continue bumping minimum Django requirements to versions that increase async support. This "latest-only" trend will continue until Django has all async features that ReactPy benefits from. After this point, ReactPy-Django will begin supporting all maintained Django versions. +- Bumped the minimum Django version to `4.2`. + +???+ note "Django 4.2+ is required" + + ReactPy-Django will continue bumping minimum Django requirements to versions that increase async support. + + This "latest-only" trend will continue until Django has all async features that ReactPy benefits from. After this point, ReactPy-Django will begin supporting all maintained Django versions. ### Removed diff --git a/README.md b/README.md index 479b6ea3..93b16ea5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - [Distributed computing](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#reactpy_default_hosts) - [Performance enhancements](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#performance-settings) - [Customizable reconnection behavior](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#stability-settings) +- [Customizable disconnection behavior](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag) - [Multiple root components](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag/) - [Django view to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#view-to-component) - [Django static file access](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-css) 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 71b174c0..e1911ca7 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -117,7 +117,7 @@ The [next step](./your-first-component.md) will show you how to create your firs Prefer a quick summary? Read the **At a Glance** section below. -!!! info "At a Glance: Your First Component" +!!! info "At a Glance" **`my_app/components.py`** diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index db0171e5..b41fc402 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -88,9 +88,11 @@ Multiprocessing-safe database used by ReactPy, typically for session data. If configuring this value, it is mandatory to enable our database router like such: -```python linenums="0" -DATABASE_ROUTERS = ["reactpy_django.database.Router", ...] -``` +=== "settings.py" + + ```python linenums="0" + DATABASE_ROUTERS = ["reactpy_django.database.Router", ...] + ``` --- @@ -145,7 +147,7 @@ Configures whether to pre-render your components via HTTP, which enables SEO com 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. +2. All [`connection` hooks](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#connection-hooks) will provide HTTP variants. 3. The component will be non-interactive until a WebSocket connection is formed. diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 6321243b..53140c13 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -27,7 +27,8 @@ This template tag can be used to insert any number of ReactPy components onto yo | `#!python class` | `#!python str | None` | The HTML class to apply to the top-level component div. | `#!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 reduces perceived latency. | `#!python "False"` | + | `#!python prerender` | `#!python str` | If `#!python "true"`, the component will pre-rendered, which enables SEO compatibility and reduces perceived latency. | `#!python "false"` | + | `#!python offline` | `#!python str` | The dotted path to a component that will be displayed if your root component loses connection to the server. Keep in mind, this `offline` component will be non-interactive (hooks won't operate). | `#!python ""` | | `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A | **Returns** @@ -62,27 +63,9 @@ This template tag can be used to insert any number of ReactPy components onto yo {% include "../../python/template-tag-bad-view.py" %} ``` - - -??? question "Can I render components on a different server (distributed computing)?" - - Yes! By using the `#!python host` keyword argument, you can render components from a completely separate ASGI server. + _Note: If you decide to not follow this warning, you will need to use the [`register_component`](../reference/utils.md#register-component) function to manually register your components._ - === "my-template.html" - - ```jinja - ... - {% component "example_project.my_app.components.do_something" host="127.0.0.1:8001" %} - ... - ``` - - This configuration most commonly involves you deploying multiple instances of your project. But, you can also create dedicated Django project(s) that only render specific ReactPy components if you wish. - - Here's a couple of things to keep in mind: - - 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 WebSocket or HTTP paths on any applications that do not perform any component rendering. - 3. Your component will only be able to access your template tag's `#!python *args`/`#!python **kwargs` if your applications share a common database. + @@ -109,7 +92,6 @@ This template tag can be used to insert any number of ReactPy components onto yo Additionally, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one component within your `#!html ` tag. - ??? question "Can I use positional arguments instead of keyword arguments?" @@ -127,4 +109,48 @@ This template tag can be used to insert any number of ReactPy components onto yo {% include "../../python/template-tag-args-kwargs.py" %} ``` - +??? question "Can I render components on a different server (distributed computing)?" + + Yes! This is most commonly done through [`settings.py:REACTPY_HOSTS`](../reference/settings.md#reactpy_default_hosts). However, you can use the `#!python host` keyword to render components on a specific ASGI server. + + === "my-template.html" + + ```jinja + ... + {% component "example_project.my_app.components.do_something" host="127.0.0.1:8001" %} + ... + ``` + + This configuration most commonly involves you deploying multiple instances of your project. But, you can also create dedicated Django project(s) that only render specific ReactPy components if you wish. + + Here's a couple of things to keep in mind: + + 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 WebSocket or HTTP paths on any applications that do not perform any component rendering. + 3. Your component will only be able to access your template tag's `#!python *args`/`#!python **kwargs` if your applications share a common database. + +??? question "How do I pre-render components for SEO compatibility?" + + This is most commonly done through [`settings.py:REACTPY_PRERENDER`](../reference/settings.md#reactpy_prerender). However, you can use the `#!python prerender` keyword to pre-render a specific component. + + === "my-template.html" + + ```jinja + ... + {% component "example_project.my_app.components.do_something" prerender="true" %} + ... + ``` + +??? question "How do I show something when the client disconnects?" + + You can use the `#!python offline` keyword to display a specific component when the client disconnects from the server. + + === "my-template.html" + + ```jinja + ... + {% component "example_project.my_app.components.do_something" offline="example_project.my_app.components.offline" %} + ... + ``` + + _Note: The `#!python offline` component will be non-interactive (hooks won't operate)._ diff --git a/src/js/package-lock.json b/src/js/package-lock.json index f4ffe4f8..97e111f1 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -1,29 +1,29 @@ { "name": "js", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^11.1.2", + "@rollup/plugin-typescript": "^11.1.6", "tslib": "^2.6.2" }, "devDependencies": { - "@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", - "typescript": "^4.9.5" + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "prettier": "^3.2.3", + "rollup": "^4.9.5", + "typescript": "^5.3.3" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, "node_modules/@reactpy/client": { @@ -40,9 +40,9 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz", - "integrity": "sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==", + "version": "25.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", + "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -50,13 +50,13 @@ "estree-walker": "^2.0.2", "glob": "^8.0.3", "is-reference": "1.2.1", - "magic-string": "^0.27.0" + "magic-string": "^0.30.3" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.68.0||^3.0.0" + "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -65,15 +65,15 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", - "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.0", + "is-builtin-module": "^3.2.1", "is-module": "^1.0.0", "resolve": "^1.22.1" }, @@ -81,7 +81,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" + "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -90,19 +90,19 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz", - "integrity": "sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", + "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.27.0" + "magic-string": "^0.30.3" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -111,18 +111,18 @@ } }, "node_modules/@rollup/plugin-typescript": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.2.tgz", - "integrity": "sha512-0ghSOCMcA7fl1JM+0gYRf+Q/HWyg+zg7/gDSc+fRLmlJWcW5K1I+CLRzaRhXf4Y3DRyPnnDo4M2ktw+a6JcDEg==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", + "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", "dependencies": { - "@rollup/pluginutils": "^5.0.1", + "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.14.0||^3.0.0", + "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, @@ -136,9 +136,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", @@ -148,7 +148,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -156,21 +156,190 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", + "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz", + "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz", + "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz", + "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz", + "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz", + "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz", + "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz", + "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", + "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz", + "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz", + "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz", + "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz", + "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "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==", + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "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==", + "version": "18.2.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", + "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -179,12 +348,12 @@ } }, "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==", + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", "dev": true, "dependencies": { - "@types/react": "^17" + "@types/react": "*" } }, "node_modules/@types/resolve": { @@ -194,9 +363,9 @@ "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==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "dev": true }, "node_modules/balanced-match": { @@ -233,15 +402,15 @@ "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==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "engines": { "node": ">=0.10.0" @@ -272,9 +441,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -286,9 +455,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/glob": { "version": "8.1.0", @@ -309,15 +481,15 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "dependencies": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" } }, "node_modules/inflight": { @@ -337,9 +509,9 @@ "dev": true }, "node_modules/is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, "dependencies": { "builtin-modules": "^3.3.0" @@ -352,11 +524,11 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -404,12 +576,12 @@ } }, "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { "node": ">=12" @@ -462,9 +634,9 @@ } }, "node_modules/prettier": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", - "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.3.tgz", + "integrity": "sha512-QNhUTBq+mqt1oH1dTfY3phOKNhcDdJkfttHI6u0kj7M2+c+7fmNKlgh2GhnHiqMcbxJ+a0j2igz/2jfl9QKLuw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -504,11 +676,11 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -520,18 +692,34 @@ } }, "node_modules/rollup": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", - "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", + "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==", "devOptional": true, + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.5", + "@rollup/rollup-android-arm64": "4.9.5", + "@rollup/rollup-darwin-arm64": "4.9.5", + "@rollup/rollup-darwin-x64": "4.9.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.5", + "@rollup/rollup-linux-arm64-gnu": "4.9.5", + "@rollup/rollup-linux-arm64-musl": "4.9.5", + "@rollup/rollup-linux-riscv64-gnu": "4.9.5", + "@rollup/rollup-linux-x64-gnu": "4.9.5", + "@rollup/rollup-linux-x64-musl": "4.9.5", + "@rollup/rollup-win32-arm64-msvc": "4.9.5", + "@rollup/rollup-win32-ia32-msvc": "4.9.5", + "@rollup/rollup-win32-x64-msvc": "4.9.5", "fsevents": "~2.3.2" } }, @@ -562,15 +750,15 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/wrappy": { @@ -579,409 +767,5 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true } - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@reactpy/client": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz", - "integrity": "sha512-mvFwAvmRMgo7lTjkhkEJzBep6HX/wfm5BaNbtEMOUzto7G/h+z1AmqlOMXLH37DSI0iwfmCuNwy07EJM0JWZ0g==", - "requires": { - "event-to-object": "^0.1.2", - "json-pointer": "^0.6.2" - } - }, - "@rollup/plugin-commonjs": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz", - "integrity": "sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.27.0" - } - }, - "@rollup/plugin-node-resolve": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", - "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.0", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - } - }, - "@rollup/plugin-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz", - "integrity": "sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.27.0" - } - }, - "@rollup/plugin-typescript": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.2.tgz", - "integrity": "sha512-0ghSOCMcA7fl1JM+0gYRf+Q/HWyg+zg7/gDSc+fRLmlJWcW5K1I+CLRzaRhXf4Y3DRyPnnDo4M2ktw+a6JcDEg==", - "requires": { - "@rollup/pluginutils": "^5.0.1", - "resolve": "^1.22.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@types/estree": { - "version": "1.0.0", - "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", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "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", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "event-to-object": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/event-to-object/-/event-to-object-0.1.2.tgz", - "integrity": "sha512-+fUmp1XOCZiYomwe5Zxp4IlchuZZfdVdjFUk5MbgRT4M+V2TEWKc0jJwKLCX/nxlJ6xM5VUb/ylzERh7YDCRrg==", - "requires": { - "json-pointer": "^0.6.2" - } - }, - "foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", - "dev": true, - "requires": { - "builtin-modules": "^3.3.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "peer": true - }, - "json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", - "requires": { - "foreach": "^2.0.4" - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "peer": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, - "prettier": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", - "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", - "dev": true - }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - } - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rollup": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", - "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", - "devOptional": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "typescript": { - "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", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - } } } diff --git a/src/js/package.json b/src/js/package.json index 0c61ec46..d4d177b2 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,27 +1,24 @@ { - "description": "reactpy-django client", - "main": "src/index.ts", + "description": "ReactPy-Django Client", + "main": "src/index.tsx", "type": "module", - "files": [ - "src/**/*.js" - ], "scripts": { "build": "rollup --config", "format": "prettier --ignore-path .gitignore --write ." }, "devDependencies": { - "@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" + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "prettier": "^3.2.3", + "rollup": "^4.9.5", + "typescript": "^5.3.3" }, "dependencies": { "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^11.1.2", + "@rollup/plugin-typescript": "^11.1.6", "tslib": "^2.6.2" } } diff --git a/src/js/rollup.config.mjs b/src/js/rollup.config.mjs index 79f93839..233de631 100644 --- a/src/js/rollup.config.mjs +++ b/src/js/rollup.config.mjs @@ -4,7 +4,7 @@ import replace from "@rollup/plugin-replace"; import typescript from "@rollup/plugin-typescript"; export default { - input: "src/index.ts", + input: "src/index.tsx", output: { file: "../reactpy_django/static/reactpy_django/client.js", format: "esm", diff --git a/src/js/src/client.ts b/src/js/src/client.ts index 6966a0f0..87a43155 100644 --- a/src/js/src/client.ts +++ b/src/js/src/client.ts @@ -12,6 +12,9 @@ export class ReactPyDjangoClient { urls: ReactPyUrls; socket: { current?: WebSocket }; + mountElement: HTMLElement | null = null; + prerenderElement: HTMLElement | null = null; + offlineElement: HTMLElement | null = null; constructor(props: ReactPyDjangoClientProps) { super(); @@ -22,7 +25,28 @@ export class ReactPyDjangoClient onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)), ...props.reconnectOptions, + onClose: () => { + // If offlineElement exists, show it and hide the mountElement/prerenderElement + if (this.prerenderElement) { + this.prerenderElement.remove(); + this.prerenderElement = null; + } + if (this.offlineElement) { + this.mountElement.hidden = true; + this.offlineElement.hidden = false; + } + }, + onOpen: () => { + // If offlineElement exists, hide it and show the mountElement + if (this.offlineElement) { + this.offlineElement.hidden = true; + this.mountElement.hidden = false; + } + }, }); + this.mountElement = props.mountElement; + this.prerenderElement = props.prerenderElement; + this.offlineElement = props.offlineElement; } sendMessage(message: any): void { diff --git a/src/js/src/index.ts b/src/js/src/index.tsx similarity index 72% rename from src/js/src/index.ts rename to src/js/src/index.tsx index 97b20efd..7bb4bfd4 100644 --- a/src/js/src/index.ts +++ b/src/js/src/index.tsx @@ -1,5 +1,8 @@ -import { mount } from "./mount"; + import { ReactPyDjangoClient } from "./client"; +import React from "react"; +import { render } from "react-dom"; +import { Layout } from "@reactpy/client/src/components"; export function mountComponent( mountElement: HTMLElement, @@ -59,8 +62,24 @@ export function mountComponent( backoffMultiplier: reconnectBackoffMultiplier, maxRetries: reconnectMaxRetries, }, + mountElement: mountElement, + prerenderElement: document.getElementById( + mountElement.id + "-prerender" + ), + offlineElement: document.getElementById(mountElement.id + "-offline"), }); + + // Replace the prerender element with the real element on the first layout update + if (client.prerenderElement) { + client.onMessage("layout-update", ({ path, model }) => { + if (client.prerenderElement) { + client.prerenderElement.replaceWith(client.mountElement); + client.prerenderElement = null; + } + }); + } + // Start rendering the component - mount(mountElement, client); + render(, client.mountElement); } diff --git a/src/js/src/mount.tsx b/src/js/src/mount.tsx deleted file mode 100644 index 4d7cdcb3..00000000 --- a/src/js/src/mount.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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/src/types.ts b/src/js/src/types.ts index b31276bc..3b7f3431 100644 --- a/src/js/src/types.ts +++ b/src/js/src/types.ts @@ -14,4 +14,7 @@ export type ReactPyUrls = { export type ReactPyDjangoClientProps = { urls: ReactPyUrls; reconnectOptions: ReconnectOptions; + mountElement: HTMLElement | null; + prerenderElement: HTMLElement | null; + offlineElement: HTMLElement | null; }; diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts index 56e231e2..0d38b94b 100644 --- a/src/js/src/utils.ts +++ b/src/js/src/utils.ts @@ -32,14 +32,14 @@ export function createReconnectingWebSocket(props: { }; socket.current.onmessage = props.onMessage; socket.current.onclose = () => { + if (props.onClose) { + props.onClose(); + } if (!everConnected) { console.info("ReactPy failed to connect!"); return; } console.info("ReactPy disconnected!"); - if (props.onClose) { - props.onClose(); - } if (retries >= maxRetries) { console.info("ReactPy connection max retries exhausted!"); return; diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 7e5ec6cb..277a7d1a 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,9 +1,10 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2022", "module": "esnext", "moduleResolution": "node", "jsx": "react", + "allowSyntheticDefaultImports": true }, "paths": { "react": [ diff --git a/src/reactpy_django/decorators.py b/src/reactpy_django/decorators.py index d06162a5..59c110b3 100644 --- a/src/reactpy_django/decorators.py +++ b/src/reactpy_django/decorators.py @@ -32,7 +32,7 @@ def auth_required( warn( "auth_required is deprecated and will be removed in the next major version. " - "An equivalent to this decorator's default is @user_passes_test('is_active').", + "An equivalent to this decorator's default is @user_passes_test(lambda user: user.is_active).", DeprecationWarning, ) diff --git a/src/reactpy_django/exceptions.py b/src/reactpy_django/exceptions.py index f52d0590..412d647f 100644 --- a/src/reactpy_django/exceptions.py +++ b/src/reactpy_django/exceptions.py @@ -6,6 +6,10 @@ class ComponentDoesNotExistError(AttributeError): ... +class OfflineComponentMissing(ComponentDoesNotExistError): + ... + + class InvalidHostError(ValueError): ... diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html index 052dc517..e7ff1311 100644 --- a/src/reactpy_django/templates/reactpy/component.html +++ b/src/reactpy_django/templates/reactpy/component.html @@ -5,7 +5,6 @@ {% endif %} {% if not reactpy_failure %} -{% if reactpy_prerender_html %}
{{ reactpy_prerender_html|safe }}
{% endif %}
+{% if reactpy_prerender_html %}
{{ reactpy_prerender_html|safe }}
{% endif %} +{% if reactpy_offline_html %} +{% endif %} {% endif %} diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 209c0ec8..8c175bc2 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -19,6 +19,7 @@ ComponentDoesNotExistError, ComponentParamError, InvalidHostError, + OfflineComponentMissing, ) from reactpy_django.types import ComponentParams from reactpy_django.utils import SyncLayout, validate_component_args @@ -38,6 +39,7 @@ def component( *args, host: str | None = None, prerender: str = str(config.REACTPY_PRERENDER), + offline: str = "", **kwargs, ): """This tag is used to embed an existing ReactPy component into your HTML template. @@ -55,6 +57,7 @@ def component( Example values include: `localhost:8000`, `example.com`, `example.com/subdir` prerender: Configures whether to pre-render this component, which \ enables SEO compatibility and reduces perceived latency. + offline: The dotted path to the component to render when the client is offline. **kwargs: The keyword arguments to provide to the component. Example :: @@ -79,6 +82,7 @@ def component( component_has_args = args or kwargs user_component: ComponentConstructor | None = None _prerender_html = "" + _offline_html = "" # Validate the host if host and config.REACTPY_DEBUG_MODE: @@ -133,6 +137,22 @@ def component( return failure_context(dotted_path, ComponentCarrierError(msg)) _prerender_html = prerender_component(user_component, args, kwargs, request) + # Fetch the offline component's HTML, if requested + if offline: + offline_component = config.REACTPY_REGISTERED_COMPONENTS.get(offline) + if not offline_component: + msg = f"Cannot render offline component '{offline}'. It is not registered as a component." + _logger.error(msg) + return failure_context(dotted_path, OfflineComponentMissing(msg)) + if not request: + msg = ( + "Cannot render an offline component without a HTTP request. Are you missing the " + "request context processor in settings.py:TEMPLATES['OPTIONS']['context_processors']?" + ) + _logger.error(msg) + return failure_context(dotted_path, ComponentCarrierError(msg)) + _offline_html = prerender_component(offline_component, [], {}, request) + # Return the template rendering context return { "reactpy_class": class_, @@ -148,6 +168,7 @@ def component( "reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_RETRIES, "reactpy_prerender_html": _prerender_html, + "reactpy_offline_html": _offline_html, } diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 10df2df9..624c4892 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -33,16 +33,18 @@ _logger = logging.getLogger(__name__) _component_tag = r"(?Pcomponent)" -_component_path = r"(?P\"[^\"'\s]+\"|'[^\"'\s]+')" -_component_kwargs = r"(?P[\s\S]*?)" +_component_path = r"""(?P"[^"'\s]+"|'[^"'\s]+')""" +_component_offline_kwarg = ( + rf"""(\s*offline\s*=\s*{_component_path.replace(r"", r"")})""" +) +_component_generic_kwarg = r"""(\s*.*?)""" COMMENT_REGEX = re.compile(r"") COMPONENT_REGEX = re.compile( r"{%\s*" + _component_tag + r"\s*" + _component_path - + r"\s*" - + _component_kwargs + + rf"({_component_offline_kwarg}|{_component_generic_kwarg})*?" + r"\s*%}" ) DATE_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -198,11 +200,17 @@ def get_components(self, templates: set[str]) -> set[str]: with open(template, "r", encoding="utf-8") as template_file: clean_template = COMMENT_REGEX.sub("", template_file.read()) regex_iterable = COMPONENT_REGEX.finditer(clean_template) - component_paths = [ - match.group("path").replace('"', "").replace("'", "") - for match in regex_iterable - ] - components.update(component_paths) + new_components: list[str] = [] + for match in regex_iterable: + new_components.append( + match.group("path").replace('"', "").replace("'", "") + ) + offline_path = match.group("offline_path") + if offline_path: + new_components.append( + offline_path.replace('"', "").replace("'", "") + ) + components.update(new_components) if not components: _logger.warning( "\033[93m" diff --git a/tests/test_app/offline/__init__.py b/tests/test_app/offline/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/offline/components.py b/tests/test_app/offline/components.py new file mode 100644 index 00000000..daa7238d --- /dev/null +++ b/tests/test_app/offline/components.py @@ -0,0 +1,15 @@ +from reactpy import component, html + + +@component +def online(): + return html.div( + {"id": "online"}, + "This is the ONLINE component. " + "Shut down your webserver and check if the offline component appears.", + ) + + +@component +def offline(): + return html.div({"id": "offline"}, "Offline") diff --git a/tests/test_app/offline/urls.py b/tests/test_app/offline/urls.py new file mode 100644 index 00000000..c9b8a236 --- /dev/null +++ b/tests/test_app/offline/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import offline + +urlpatterns = [ + path("offline/", offline), +] diff --git a/tests/test_app/offline/views.py b/tests/test_app/offline/views.py new file mode 100644 index 00000000..ae33caed --- /dev/null +++ b/tests/test_app/offline/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + + +def offline(request): + return render(request, "offline.html", {}) diff --git a/tests/test_app/templates/offline.html b/tests/test_app/templates/offline.html new file mode 100644 index 00000000..e7c39106 --- /dev/null +++ b/tests/test_app/templates/offline.html @@ -0,0 +1,20 @@ +{% load static %} {% load reactpy %} + + + + + + + + + ReactPy + + + +

ReactPy Offline Test Page

+
+ {% component "test_app.offline.components.online" offline="test_app.offline.components.offline" %} +
+ + + diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index ca35cf50..61a0ca86 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -46,12 +46,18 @@ def setUpClass(cls): cls._server_process.ready.wait() cls._port = cls._server_process.port.value - # Open the second server process + # Open the second server process, used for testing custom hosts cls._server_process2 = cls.ProtocolServerProcess(cls.host, get_application) cls._server_process2.start() cls._server_process2.ready.wait() cls._port2 = cls._server_process2.port.value + # Open the third server process, used for testing offline fallback + cls._server_process3 = cls.ProtocolServerProcess(cls.host, get_application) + cls._server_process3.start() + cls._server_process3.ready.wait() + cls._port3 = cls._server_process3.port.value + # Open a Playwright browser window if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) @@ -67,9 +73,11 @@ def tearDownClass(cls): # Close the Playwright browser cls.playwright.stop() - # Close the second server process + # Close the other server processes cls._server_process2.terminate() cls._server_process2.join() + cls._server_process3.terminate() + cls._server_process3.join() # Repurposed from ChannelsLiveServerTestCase._post_teardown cls._server_process.terminate() @@ -600,3 +608,20 @@ def test_url_router(self): finally: new_page.close() + + def test_offline_components(self): + new_page = self.browser.new_page() + try: + server3_url = self.live_server_url.replace( + str(self._port), str(self._port3) + ) + new_page.goto(f"{server3_url}/offline/") + new_page.wait_for_selector("div:not([hidden]) > #online") + self.assertIsNotNone(new_page.query_selector("div[hidden] > #offline")) + self._server_process3.terminate() + self._server_process3.join() + new_page.wait_for_selector("div:not([hidden]) > #offline") + self.assertIsNotNone(new_page.query_selector("div[hidden] > #online")) + + finally: + new_page.close() diff --git a/tests/test_app/tests/test_regex.py b/tests/test_app/tests/test_regex.py index 61b72e4a..07a0dbfd 100644 --- a/tests/test_app/tests/test_regex.py +++ b/tests/test_app/tests/test_regex.py @@ -1,5 +1,6 @@ -from django.test import TestCase +import re +from django.test import TestCase from reactpy_django.utils import COMMENT_REGEX, COMPONENT_REGEX @@ -28,6 +29,15 @@ def test_component_regex(self): %}""", # noqa: W291 COMPONENT_REGEX, ) + self.assertRegex(r'{% component "my.component" my_object %}', COMPONENT_REGEX) + self.assertRegex( + r'{% component "my.component" class="example-cls" x=123 y=456 %}', + COMPONENT_REGEX, + ) + self.assertRegex( + r'{% component "my.component" class = "example-cls" %}', + COMPONENT_REGEX, + ) # Fake component matches self.assertNotRegex(r'{% not_a_real_thing "my.component" %}', COMPONENT_REGEX) @@ -134,3 +144,26 @@ def test_comment_regex(self): ), "", ) + + def test_offline_component_regex(self): + regex = re.compile(COMPONENT_REGEX) + # Check if "offline_path" group is present and equals to "my_offline_path" + search = regex.search( + r'{% component "my.component" offline="my_offline_path" %}' + ) + self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore + + search = regex.search( + r'{% component "my.component" arg_1="1" offline="my_offline_path" arg_2="2" %}' + ) + self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore + + search = regex.search( + r'{% component "my.component" offline="my_offline_path" arg_2="2" %}' + ) + + self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore + search = regex.search( + r'{% component "my.component" arg_1="1" offline="my_offline_path" %}' + ) + self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py index 50cc5999..d15a2817 100644 --- a/tests/test_app/urls.py +++ b/tests/test_app/urls.py @@ -30,6 +30,7 @@ path("", include("test_app.prerender.urls")), path("", include("test_app.performance.urls")), path("", include("test_app.router.urls")), + path("", include("test_app.offline.urls")), path("reactpy/", include("reactpy_django.http.urls")), path("admin/", admin.site.urls), ]