diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..170d7ddb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +end_of_line = lf + +[*.py] +indent_size = 4 +max_line_length = 120 + +[*.yml] +indent_size = 4 + +[*.md] +indent_size = 4 + +[*.html] +indent_size = 4 +max_line_length = off + +[*.js] +max_line_length = off + +[*.css] +indent_size = 4 +max_line_length = off + +# Tests can violate line width restrictions in the interest of clarity. +[**/test_*.py] +max_line_length = off diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 17080b52..63a412e0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -72,6 +72,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index b79d3cd2..43d114af 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -5,19 +5,25 @@ on: branches: - main jobs: - deploy: + publish-develop-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install -r requirements/build-docs.txt - - name: Publish Develop Docs + - name: Install dependencies + run: pip install hatch + - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - cd docs - mike deploy --push develop + - name: Publish Develop Docs + run: hatch run docs:deploy_develop + concurrency: + group: publish-docs diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-latest-docs.yml similarity index 56% rename from .github/workflows/publish-release-docs.yml rename to .github/workflows/publish-latest-docs.yml index a98e9869..a4945b6f 100644 --- a/.github/workflows/publish-release-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -1,23 +1,29 @@ -name: Publish Release Docs +name: Publish Latest Docs on: release: types: [published] jobs: - deploy: + publish-latest-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install -r requirements/build-docs.txt - - name: Publish ${{ github.event.release.name }} Docs + - name: Install dependencies + run: pip install hatch + - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - cd docs - mike deploy --push --update-aliases ${{ github.event.release.name }} latest + - name: Publish ${{ github.event.release.name }} Docs + run: hatch run docs:deploy_latest ${{ github.ref_name }} + concurrency: + group: publish-docs diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-py.yml deleted file mode 100644 index 72a04dae..00000000 --- a/.github/workflows/publish-py.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Publish Python - -on: - release: - types: [published] - -jobs: - release-package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements/build-pkg.txt - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python -m build --sdist --wheel --outdir dist . - twine upload dist/* diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml new file mode 100644 index 00000000..e20affbb --- /dev/null +++ b/.github/workflows/publish-python.yml @@ -0,0 +1,27 @@ +name: Publish Python + +on: + release: + types: [published] + +jobs: + publish-python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: pip install hatch + - name: Build Package + run: hatch build --clean + - name: Publish to PyPI + env: + HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }} + run: hatch publish --yes diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 907e1a2c..5a2d4fd4 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -7,8 +7,6 @@ on: pull_request: branches: - main - schedule: - - cron: "0 0 * * *" jobs: docs: @@ -17,20 +15,18 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - uses: actions/setup-python@v5 with: python-version: 3.x + - name: Install Python Dependencies + run: pip install hatch + # DISABLED DUE TO DJANGO DOCS CONSTANTLY THROWING 429 ERRORS + # - name: Check documentation links + # run: hatch run docs:linkcheck - name: Check docs build - run: | - pip install -r requirements/build-docs.txt - linkcheckMarkdown docs/ -v -r - linkcheckMarkdown README.md -v -r - linkcheckMarkdown CHANGELOG.md -v -r - cd docs - mkdocs build --strict + run: hatch run docs:build - name: Check docs examples - run: | - pip install -r requirements/check-types.txt - pip install -r requirements/check-style.txt - mypy --show-error-codes docs/examples/python/ - ruff check docs/examples/python/ + run: hatch fmt docs --check diff --git a/.github/workflows/test-javascript.yml b/.github/workflows/test-javascript.yml new file mode 100644 index 00000000..8e204dcb --- /dev/null +++ b/.github/workflows/test-javascript.yml @@ -0,0 +1,25 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + javascript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install hatch + - name: Run Tests + run: hatch run javascript:check diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml new file mode 100644 index 00000000..ac8d77b6 --- /dev/null +++ b/.github/workflows/test-python.yml @@ -0,0 +1,63 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + python-source: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + settings-module: ["single_db", "multi_db"] + operating-system: ["ubuntu-latest", "windows-latest"] + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python Dependencies + run: pip install hatch + - name: Run Single DB Tests + run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_${{matrix.settings-module}} -v + + python-formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install hatch + - name: Check Python formatting + run: hatch fmt src tests --check + + python-types: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install hatch + - name: Run Python type checker + run: hatch run python:type_check diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml deleted file mode 100644 index 328bd1c3..00000000 --- a/.github/workflows/test-src.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Test - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "0 0 * * *" - -jobs: - source: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - steps: - - uses: actions/checkout@v4 - - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install Python Dependencies - run: pip install -r requirements/test-run.txt - - name: Run Tests - run: nox -t test diff --git a/.gitignore b/.gitignore index ffabb7fc..e675e09a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # ReactPy-Django Build Artifacts -src/reactpy_django/static/reactpy_django/client.js +src/reactpy_django/static/reactpy_django/index.js +src/reactpy_django/static/reactpy_django/index.js.map src/reactpy_django/static/reactpy_django/pyscript src/reactpy_django/static/reactpy_django/morphdom @@ -101,6 +102,7 @@ venv.bak/ # mkdocs documentation /site +docs/site # mypy .mypy_cache/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..32ad81f3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "never", + "trailingComma": "all" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 620c1f75..f3a0ff04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,33 +10,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + + + +## [Unreleased] + +### Changed + +- Updated the interface for `reactpy.hooks.use_channel_layer` to be more intuitive. + - Arguments now must be provided as keyworded arguments. + - The `name` argument has been renamed to `channel`. + - The `group_name` argument has been renamed to `group`. + - The `group_add` and `group_discard` arguments have been removed for simplicity. + +### [5.2.1] - 2025-01-10 + +### Changed + +- Use the latest version of `@reactpy/client` which includes a fix for needless client-side component re-creation. + +### [5.2.0] - 2024-12-29 ### Added -- for new features. + +- User login/logout features! + - `reactpy_django.hooks.use_auth` to provide **persistent** `login` and `logout` functionality to your components. + - `settings.py:REACTPY_AUTH_TOKEN_MAX_AGE` to control the maximum seconds before ReactPy's login token expires. + - `settings.py:REACTPY_CLEAN_AUTH_TOKENS` to control whether ReactPy should clean up expired authentication tokens during automatic cleanups. +- Automatically convert Django forms to ReactPy forms via the new `reactpy_django.components.django_form` component! +- The ReactPy component tree can now be forcibly re-rendered via the new `reactpy_django.hooks.use_rerender` hook. ### Changed -- for changes in existing functionality. -### Deprecated -- for soon-to-be removed features. +- Refactoring of internal code to improve maintainability. No changes to publicly documented API. -### Removed -- for removed features. +### Fixed + +- Fixed bug where pre-rendered components could generate a `SynchronousOnlyOperation` exception if they access a freshly logged out Django user object. + +## [5.1.1] - 2024-12-02 ### Fixed -- for bug fixes. -### Security -- for vulnerability fixes. - --> +- Fixed regression from the previous release where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. - +### Changed -## [Unreleased] +- Set upper limit on ReactPy version to `<2.0.0`. +- ReactPy web modules are now streamed in chunks. +- ReactPy web modules are now streamed using asynchronous file reading to improve performance. +- Performed refactoring to utilize `ruff` as this repository's linter. + +## [5.1.0] - 2024-11-24 + +### Added + +- `settings.py:REACTPY_ASYNC_RENDERING` to enable asynchronous rendering of components. + +### Changed + +- Bumped the minimum ReactPy version to `1.1.0`. + +## [5.0.0] - 2024-10-22 + +### Changed + +- Now using ReactPy-Router v1 for URL routing, which comes with a slightly different API than before. +- Removed dependency on `aiofile`. + +### Removed -- Nothing (yet)! +- Removed the following **deprecated** features: + - The `compatibility` argument on `reactpy_django.components.view_to_component` + - `reactpy_django.components.view_to_component` **usage as a decorator** + - `reactpy_django.decorators.auth_required` + - `reactpy_django.REACTPY_WEBSOCKET_PATH` + - `settings.py:REACTPY_WEBSOCKET_URL` -## [4.0.0] +## [4.0.0] - 2024-06-22 ### Added @@ -110,8 +165,8 @@ Using the following categories, list your changes in this order: - New Django `User` related features! - `reactpy_django.hooks.use_user` can be used to access the current user. - `reactpy_django.hooks.use_user_data` provides a simplified interface for storing user key-value data. - - `reactpy_django.decorators.user_passes_test` is inspired by the [equivalent Django decorator](http://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test), but ours works with ReactPy components. - - `settings.py:REACTPY_AUTO_RELOGIN` will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. This is useful to continuously update `last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/dev/topics/http/sessions/). + - `reactpy_django.decorators.user_passes_test` is inspired by the [equivalent Django decorator](http://docs.djangoproject.com/en/stable/topics/auth/default/#django.contrib.auth.decorators.user_passes_test), but ours works with ReactPy components. + - `settings.py:REACTPY_AUTO_RELOGIN` will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. This is useful to continuously update `last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/stable/topics/http/sessions/). ### Changed @@ -501,7 +556,12 @@ Using the following categories, list your changes in this order: - Support for IDOM within the Django -[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.2.1...HEAD +[5.2.1]: https://github.com/reactive-python/reactpy-django/compare/5.2.0...5.2.1 +[5.2.0]: https://github.com/reactive-python/reactpy-django/compare/5.1.1...5.2.0 +[5.1.1]: https://github.com/reactive-python/reactpy-django/compare/5.1.0...5.1.1 +[5.1.0]: https://github.com/reactive-python/reactpy-django/compare/5.0.0...5.1.0 +[5.0.0]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...5.0.0 [4.0.0]: https://github.com/reactive-python/reactpy-django/compare/3.8.1...4.0.0 [3.8.1]: https://github.com/reactive-python/reactpy-django/compare/3.8.0...3.8.1 [3.8.0]: https://github.com/reactive-python/reactpy-django/compare/3.7.0...3.8.0 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ddcb7f8d..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include src/reactpy_django/py.typed -recursive-include src/reactpy_django/static * -recursive-include src/reactpy_django/templates *.html diff --git a/README.md b/README.md index 817e684b..3ebd3faf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ReactPy-Django

- - + + @@ -28,8 +28,9 @@ - [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/) -- [Cross-process communication/signaling (Channel Layers)](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-channel-layer) +- [Cross-process communication/signaling](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-channel-layer) - [Django view to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#view-to-component) +- [Django form to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-form) - [Django static file access](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-css) - [Django database access](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-query) @@ -83,7 +84,7 @@ def hello_world(recipient: str): -## [`my_app/templates/my_template.html`](https://docs.djangoproject.com/en/dev/topics/templates/) +## [`my_app/templates/my_template.html`](https://docs.djangoproject.com/en/stable/topics/templates/) diff --git a/docs/examples/html/django_form_bootstrap.html b/docs/examples/html/django_form_bootstrap.html new file mode 100644 index 00000000..6aba84ca --- /dev/null +++ b/docs/examples/html/django_form_bootstrap.html @@ -0,0 +1,11 @@ +{% load django_bootstrap5 %} + + +{% bootstrap_css %} +{% bootstrap_javascript %} + + +{% bootstrap_form form %} +{% bootstrap_button button_type="submit" content="OK" %} +{% bootstrap_button button_type="reset" content="Reset" %} diff --git a/docs/examples/html/pyscript-component.html b/docs/examples/html/pyscript_component.html similarity index 100% rename from docs/examples/html/pyscript-component.html rename to docs/examples/html/pyscript_component.html diff --git a/docs/examples/html/pyscript-initial-object.html b/docs/examples/html/pyscript_initial_object.html similarity index 100% rename from docs/examples/html/pyscript-initial-object.html rename to docs/examples/html/pyscript_initial_object.html diff --git a/docs/examples/html/pyscript-initial-string.html b/docs/examples/html/pyscript_initial_string.html similarity index 100% rename from docs/examples/html/pyscript-initial-string.html rename to docs/examples/html/pyscript_initial_string.html diff --git a/docs/examples/html/pyscript-js-module.html b/docs/examples/html/pyscript_local_import.html similarity index 100% rename from docs/examples/html/pyscript-js-module.html rename to docs/examples/html/pyscript_local_import.html diff --git a/docs/examples/html/pyscript-multiple-files.html b/docs/examples/html/pyscript_multiple_files.html similarity index 100% rename from docs/examples/html/pyscript-multiple-files.html rename to docs/examples/html/pyscript_multiple_files.html diff --git a/docs/examples/html/pyscript-root.html b/docs/examples/html/pyscript_root.html similarity index 100% rename from docs/examples/html/pyscript-root.html rename to docs/examples/html/pyscript_root.html diff --git a/docs/examples/html/pyscript-setup.html b/docs/examples/html/pyscript_setup.html similarity index 100% rename from docs/examples/html/pyscript-setup.html rename to docs/examples/html/pyscript_setup.html diff --git a/docs/examples/html/pyscript-setup-config-object.html b/docs/examples/html/pyscript_setup_config_object.html similarity index 100% rename from docs/examples/html/pyscript-setup-config-object.html rename to docs/examples/html/pyscript_setup_config_object.html diff --git a/docs/examples/html/pyscript-setup-config-string.html b/docs/examples/html/pyscript_setup_config_string.html similarity index 100% rename from docs/examples/html/pyscript-setup-config-string.html rename to docs/examples/html/pyscript_setup_config_string.html diff --git a/docs/examples/html/pyscript-setup-dependencies.html b/docs/examples/html/pyscript_setup_dependencies.html similarity index 100% rename from docs/examples/html/pyscript-setup-dependencies.html rename to docs/examples/html/pyscript_setup_dependencies.html diff --git a/docs/examples/html/pyscript-setup-extra-js-object.html b/docs/examples/html/pyscript_setup_extra_js_object.html similarity index 100% rename from docs/examples/html/pyscript-setup-extra-js-object.html rename to docs/examples/html/pyscript_setup_extra_js_object.html diff --git a/docs/examples/html/pyscript-setup-extra-js-string.html b/docs/examples/html/pyscript_setup_extra_js_string.html similarity index 100% rename from docs/examples/html/pyscript-setup-extra-js-string.html rename to docs/examples/html/pyscript_setup_extra_js_string.html diff --git a/docs/examples/html/pyscript_setup_local_interpreter.html b/docs/examples/html/pyscript_setup_local_interpreter.html new file mode 100644 index 00000000..8371fa94 --- /dev/null +++ b/docs/examples/html/pyscript_setup_local_interpreter.html @@ -0,0 +1 @@ +{% pyscript_setup config='{"interpreter":"/static/pyodide/pyodide.mjs"}' %} diff --git a/docs/examples/html/pyscript-ssr-parent.html b/docs/examples/html/pyscript_ssr_parent.html similarity index 100% rename from docs/examples/html/pyscript-ssr-parent.html rename to docs/examples/html/pyscript_ssr_parent.html diff --git a/docs/examples/html/pyscript-tag.html b/docs/examples/html/pyscript_tag.html similarity index 100% rename from docs/examples/html/pyscript-tag.html rename to docs/examples/html/pyscript_tag.html diff --git a/docs/examples/python/configure-asgi.py b/docs/examples/python/configure_asgi.py similarity index 77% rename from docs/examples/python/configure-asgi.py rename to docs/examples/python/configure_asgi.py index 8081d747..8feb0ec2 100644 --- a/docs/examples/python/configure-asgi.py +++ b/docs/examples/python/configure_asgi.py @@ -10,11 +10,10 @@ from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 + from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402 -application = ProtocolTypeRouter( - { - "http": django_asgi_app, - "websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]), - } -) +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]), +}) diff --git a/docs/examples/python/configure-asgi-middleware.py b/docs/examples/python/configure_asgi_middleware.py similarity index 60% rename from docs/examples/python/configure-asgi-middleware.py rename to docs/examples/python/configure_asgi_middleware.py index 6df35a39..0c5a7214 100644 --- a/docs/examples/python/configure-asgi-middleware.py +++ b/docs/examples/python/configure_asgi_middleware.py @@ -1,5 +1,6 @@ # Broken load order, only used for linting from channels.routing import ProtocolTypeRouter, URLRouter + from reactpy_django import REACTPY_WEBSOCKET_ROUTE django_asgi_app = "" @@ -8,9 +9,7 @@ # start from channels.auth import AuthMiddlewareStack # noqa: E402 -application = ProtocolTypeRouter( - { - "http": django_asgi_app, - "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), - } -) +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), +}) diff --git a/docs/examples/python/configure-channels-asgi-app.py b/docs/examples/python/configure_channels_asgi_app.py similarity index 100% rename from docs/examples/python/configure-channels-asgi-app.py rename to docs/examples/python/configure_channels_asgi_app.py diff --git a/docs/examples/python/configure-channels-installed-app.py b/docs/examples/python/configure_channels_installed_app.py similarity index 100% rename from docs/examples/python/configure-channels-installed-app.py rename to docs/examples/python/configure_channels_installed_app.py diff --git a/docs/examples/python/configure-installed-apps.py b/docs/examples/python/configure_installed_apps.py similarity index 100% rename from docs/examples/python/configure-installed-apps.py rename to docs/examples/python/configure_installed_apps.py diff --git a/docs/examples/python/configure-urls.py b/docs/examples/python/configure_urls.py similarity index 100% rename from docs/examples/python/configure-urls.py rename to docs/examples/python/configure_urls.py diff --git a/docs/examples/python/django-css.py b/docs/examples/python/django_css.py similarity index 99% rename from docs/examples/python/django-css.py rename to docs/examples/python/django_css.py index aeb4addb..c7f60881 100644 --- a/docs/examples/python/django-css.py +++ b/docs/examples/python/django_css.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import django_css diff --git a/docs/examples/python/django-css-external-link.py b/docs/examples/python/django_css_external_link.py similarity index 53% rename from docs/examples/python/django-css-external-link.py rename to docs/examples/python/django_css_external_link.py index ac1d0fba..28eb3fca 100644 --- a/docs/examples/python/django-css-external-link.py +++ b/docs/examples/python/django_css_external_link.py @@ -4,8 +4,6 @@ @component def my_component(): return html.div( - html.link( - {"rel": "stylesheet", "href": "https://example.com/external-styles.css"} - ), + html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}), html.button("My Button!"), ) diff --git a/docs/examples/python/django-css-local-link.py b/docs/examples/python/django_css_local_link.py similarity index 100% rename from docs/examples/python/django-css-local-link.py rename to docs/examples/python/django_css_local_link.py diff --git a/docs/examples/python/django_form.py b/docs/examples/python/django_form.py new file mode 100644 index 00000000..51960db1 --- /dev/null +++ b/docs/examples/python/django_form.py @@ -0,0 +1,10 @@ +from reactpy import component, html + +from example.forms import MyForm +from reactpy_django.components import django_form + + +@component +def basic_form(): + children = [html.input({"type": "submit"})] + return django_form(MyForm, bottom_children=children) diff --git a/docs/examples/python/django_form_bootstrap.py b/docs/examples/python/django_form_bootstrap.py new file mode 100644 index 00000000..449e1cc4 --- /dev/null +++ b/docs/examples/python/django_form_bootstrap.py @@ -0,0 +1,9 @@ +from reactpy import component + +from example.forms import MyForm +from reactpy_django.components import django_form + + +@component +def basic_form(): + return django_form(MyForm, form_template="bootstrap_form.html") diff --git a/docs/examples/python/django_form_class.py b/docs/examples/python/django_form_class.py new file mode 100644 index 00000000..e556295e --- /dev/null +++ b/docs/examples/python/django_form_class.py @@ -0,0 +1,5 @@ +from django import forms + + +class MyForm(forms.Form): + username = forms.CharField(label="Username") diff --git a/docs/examples/python/django_form_on_success.py b/docs/examples/python/django_form_on_success.py new file mode 100644 index 00000000..d8b6927c --- /dev/null +++ b/docs/examples/python/django_form_on_success.py @@ -0,0 +1,21 @@ +from reactpy import component, hooks, html +from reactpy_router import navigate + +from example.forms import MyForm +from reactpy_django.components import django_form +from reactpy_django.types import FormEventData + + +@component +def basic_form(): + submitted, set_submitted = hooks.use_state(False) + + def on_submit(event: FormEventData): + """This function will be called when the form is successfully submitted.""" + set_submitted(True) + + if submitted: + return navigate("/homepage") + + children = [html.input({"type": "submit"})] + return django_form(MyForm, on_success=on_submit, bottom_children=children) diff --git a/docs/examples/python/django-js.py b/docs/examples/python/django_js.py similarity index 99% rename from docs/examples/python/django-js.py rename to docs/examples/python/django_js.py index b4af014c..37868184 100644 --- a/docs/examples/python/django-js.py +++ b/docs/examples/python/django_js.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import django_js diff --git a/docs/examples/python/django-js-local-script.py b/docs/examples/python/django_js_local_script.py similarity index 100% rename from docs/examples/python/django-js-local-script.py rename to docs/examples/python/django_js_local_script.py diff --git a/docs/examples/python/django-js-remote-script.py b/docs/examples/python/django_js_remote_script.py similarity index 100% rename from docs/examples/python/django-js-remote-script.py rename to docs/examples/python/django_js_remote_script.py diff --git a/docs/examples/python/django-query-postprocessor.py b/docs/examples/python/django_query_postprocessor.py similarity index 99% rename from docs/examples/python/django-query-postprocessor.py rename to docs/examples/python/django_query_postprocessor.py index da33c362..7bdc870c 100644 --- a/docs/examples/python/django-query-postprocessor.py +++ b/docs/examples/python/django_query_postprocessor.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component + +from example.models import TodoItem from reactpy_django.hooks import use_query from reactpy_django.utils import django_query_postprocessor diff --git a/docs/examples/python/django-router.py b/docs/examples/python/django_router.py similarity index 92% rename from docs/examples/python/django-router.py rename to docs/examples/python/django_router.py index 5c845967..721eb939 100644 --- a/docs/examples/python/django-router.py +++ b/docs/examples/python/django_router.py @@ -1,7 +1,8 @@ from reactpy import component, html -from reactpy_django.router import django_router from reactpy_router import route +from reactpy_django.router import django_router + @component def my_component(): @@ -14,5 +15,5 @@ def my_component(): route("/router/string//", html.div("Example 6")), route("/router/uuid//", html.div("Example 7")), route("/router/two_values///", html.div("Example 8")), - route("/router/*", html.div("Fallback")), + route("/router/", html.div("Fallback")), ) diff --git a/docs/examples/python/example/__init__.py b/docs/examples/python/example/__init__.py index e69de29b..c32d6329 100644 --- a/docs/examples/python/example/__init__.py +++ b/docs/examples/python/example/__init__.py @@ -0,0 +1,3 @@ +"""This module exists only to satisfy type checkers. + +Do not use the files in this module as examples within the docs.""" diff --git a/docs/examples/python/example/components.py b/docs/examples/python/example/components.py new file mode 100644 index 00000000..ec301524 --- /dev/null +++ b/docs/examples/python/example/components.py @@ -0,0 +1,6 @@ +"""This module exists only to satisfy type checkers. + +Do not use the files in this module as examples within the docs.""" + + +def child_component(): ... diff --git a/docs/examples/python/example/forms.py b/docs/examples/python/example/forms.py new file mode 100644 index 00000000..8d3eefc0 --- /dev/null +++ b/docs/examples/python/example/forms.py @@ -0,0 +1,4 @@ +from django import forms + + +class MyForm(forms.Form): ... diff --git a/docs/examples/python/example/models.py b/docs/examples/python/example/models.py index ec98be92..2bf062b9 100644 --- a/docs/examples/python/example/models.py +++ b/docs/examples/python/example/models.py @@ -1,5 +1,4 @@ -from django.db.models import CharField, Model +from django.db import models -class TodoItem(Model): - text: CharField = CharField(max_length=255) +class TodoItem(models.Model): ... diff --git a/docs/examples/python/example/views.py b/docs/examples/python/example/views.py index 23e21130..49bfeb8e 100644 --- a/docs/examples/python/example/views.py +++ b/docs/examples/python/example/views.py @@ -1,5 +1,8 @@ -from django.shortcuts import render +"""This module exists only to satisfy type checkers. +Do not use the files in this module as examples within the docs.""" -def index(request): - return render(request, "my_template.html") +from python.hello_world_cbv import HelloWorld +from python.hello_world_fbv import hello_world + +__all__ = ["HelloWorld", "hello_world"] diff --git a/docs/examples/python/example/urls.py b/docs/examples/python/first_urls.py similarity index 99% rename from docs/examples/python/example/urls.py rename to docs/examples/python/first_urls.py index 74f72806..a0f1d72f 100644 --- a/docs/examples/python/example/urls.py +++ b/docs/examples/python/first_urls.py @@ -1,4 +1,5 @@ from django.urls import path + from example import views urlpatterns = [ diff --git a/docs/examples/python/first_view.py b/docs/examples/python/first_view.py new file mode 100644 index 00000000..23e21130 --- /dev/null +++ b/docs/examples/python/first_view.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + + +def index(request): + return render(request, "my_template.html") diff --git a/docs/examples/python/hello_world_app_config_cbv.py b/docs/examples/python/hello_world_app_config_cbv.py index ec448117..c0852da8 100644 --- a/docs/examples/python/hello_world_app_config_cbv.py +++ b/docs/examples/python/hello_world_app_config_cbv.py @@ -1,7 +1,7 @@ from django.apps import AppConfig -from reactpy_django.utils import register_iframe -from . import views +from example import views +from reactpy_django.utils import register_iframe class ExampleAppConfig(AppConfig): diff --git a/docs/examples/python/hello_world_app_config_fbv.py b/docs/examples/python/hello_world_app_config_fbv.py index c23c6919..47a71cde 100644 --- a/docs/examples/python/hello_world_app_config_fbv.py +++ b/docs/examples/python/hello_world_app_config_fbv.py @@ -1,7 +1,7 @@ from django.apps import AppConfig -from reactpy_django.utils import register_iframe -from . import views +from example import views +from reactpy_django.utils import register_iframe class ExampleAppConfig(AppConfig): diff --git a/docs/examples/python/pyscript-js-execution.py b/docs/examples/python/pyodide_js_module.py similarity index 58% rename from docs/examples/python/pyscript-js-execution.py rename to docs/examples/python/pyodide_js_module.py index a96ef65b..864936dc 100644 --- a/docs/examples/python/pyscript-js-execution.py +++ b/docs/examples/python/pyodide_js_module.py @@ -4,8 +4,7 @@ @component def root(): - - def onClick(event): + def on_click(event): js.document.title = "New window title" - return html.button({"onClick": onClick}, "Click Me!") + return html.button({"onClick": on_click}, "Click Me!") diff --git a/docs/examples/python/pyscript-component-initial-object.py b/docs/examples/python/pyscript_component_initial_object.py similarity index 99% rename from docs/examples/python/pyscript-component-initial-object.py rename to docs/examples/python/pyscript_component_initial_object.py index 222a568b..d84328a4 100644 --- a/docs/examples/python/pyscript-component-initial-object.py +++ b/docs/examples/python/pyscript_component_initial_object.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-component-initial-string.py b/docs/examples/python/pyscript_component_initial_string.py similarity index 99% rename from docs/examples/python/pyscript-component-initial-string.py rename to docs/examples/python/pyscript_component_initial_string.py index 664b9f9b..bb8f9d17 100644 --- a/docs/examples/python/pyscript-component-initial-string.py +++ b/docs/examples/python/pyscript_component_initial_string.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-component-multiple-files-root.py b/docs/examples/python/pyscript_component_multiple_files_root.py similarity index 99% rename from docs/examples/python/pyscript-component-multiple-files-root.py rename to docs/examples/python/pyscript_component_multiple_files_root.py index 776b26b2..fd826137 100644 --- a/docs/examples/python/pyscript-component-multiple-files-root.py +++ b/docs/examples/python/pyscript_component_multiple_files_root.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-component-root.py b/docs/examples/python/pyscript_component_root.py similarity index 99% rename from docs/examples/python/pyscript-component-root.py rename to docs/examples/python/pyscript_component_root.py index 9880b740..3d795247 100644 --- a/docs/examples/python/pyscript-component-root.py +++ b/docs/examples/python/pyscript_component_root.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript_ffi.py b/docs/examples/python/pyscript_ffi.py new file mode 100644 index 00000000..d744dd88 --- /dev/null +++ b/docs/examples/python/pyscript_ffi.py @@ -0,0 +1,14 @@ +from pyscript import document, window +from reactpy import component, html + + +@component +def root(): + def on_click(event): + my_element = document.querySelector("#example") + my_element.innerText = window.location.hostname + + return html.div( + {"id": "example"}, + html.button({"onClick": on_click}, "Click Me!"), + ) diff --git a/docs/examples/python/pyscript-hello-world.py b/docs/examples/python/pyscript_hello_world.py similarity index 100% rename from docs/examples/python/pyscript-hello-world.py rename to docs/examples/python/pyscript_hello_world.py diff --git a/docs/examples/python/pyscript-initial-object.py b/docs/examples/python/pyscript_initial_object.py similarity index 100% rename from docs/examples/python/pyscript-initial-object.py rename to docs/examples/python/pyscript_initial_object.py diff --git a/docs/examples/python/pyscript-js-module.py b/docs/examples/python/pyscript_local_import.py similarity index 100% rename from docs/examples/python/pyscript-js-module.py rename to docs/examples/python/pyscript_local_import.py diff --git a/docs/examples/python/pyscript-multiple-files-child.py b/docs/examples/python/pyscript_multiple_files_child.py similarity index 100% rename from docs/examples/python/pyscript-multiple-files-child.py rename to docs/examples/python/pyscript_multiple_files_child.py diff --git a/docs/examples/python/pyscript-multiple-files-root.py b/docs/examples/python/pyscript_multiple_files_root.py similarity index 60% rename from docs/examples/python/pyscript-multiple-files-root.py rename to docs/examples/python/pyscript_multiple_files_root.py index dc17e7ad..9ae8e549 100644 --- a/docs/examples/python/pyscript-multiple-files-root.py +++ b/docs/examples/python/pyscript_multiple_files_root.py @@ -1,9 +1,6 @@ -from typing import TYPE_CHECKING - from reactpy import component, html -if TYPE_CHECKING: - from .child import child_component +from example.components import child_component @component diff --git a/docs/examples/python/pyscript-root.py b/docs/examples/python/pyscript_root.py similarity index 100% rename from docs/examples/python/pyscript-root.py rename to docs/examples/python/pyscript_root.py diff --git a/docs/examples/python/pyscript-setup-config-object.py b/docs/examples/python/pyscript_setup_config_object.py similarity index 100% rename from docs/examples/python/pyscript-setup-config-object.py rename to docs/examples/python/pyscript_setup_config_object.py diff --git a/docs/examples/python/pyscript-setup-extra-js-object.py b/docs/examples/python/pyscript_setup_extra_js_object.py similarity index 100% rename from docs/examples/python/pyscript-setup-extra-js-object.py rename to docs/examples/python/pyscript_setup_extra_js_object.py diff --git a/docs/examples/python/pyscript-ssr-child.py b/docs/examples/python/pyscript_ssr_child.py similarity index 100% rename from docs/examples/python/pyscript-ssr-child.py rename to docs/examples/python/pyscript_ssr_child.py diff --git a/docs/examples/python/pyscript-ssr-parent.py b/docs/examples/python/pyscript_ssr_parent.py similarity index 99% rename from docs/examples/python/pyscript-ssr-parent.py rename to docs/examples/python/pyscript_ssr_parent.py index b51aa110..524cdc52 100644 --- a/docs/examples/python/pyscript-ssr-parent.py +++ b/docs/examples/python/pyscript_ssr_parent.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-tag.py b/docs/examples/python/pyscript_tag.py similarity index 99% rename from docs/examples/python/pyscript-tag.py rename to docs/examples/python/pyscript_tag.py index 6455e9da..a038b267 100644 --- a/docs/examples/python/pyscript-tag.py +++ b/docs/examples/python/pyscript_tag.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.html import pyscript example_source_code = """ diff --git a/docs/examples/python/register-component.py b/docs/examples/python/register_component.py similarity index 99% rename from docs/examples/python/register-component.py rename to docs/examples/python/register_component.py index cbdbf789..6d7d3831 100644 --- a/docs/examples/python/register-component.py +++ b/docs/examples/python/register_component.py @@ -1,4 +1,5 @@ from django.apps import AppConfig + from reactpy_django.utils import register_component diff --git a/docs/examples/python/template-tag-args-kwargs.py b/docs/examples/python/template_tag_args_kwargs.py similarity index 100% rename from docs/examples/python/template-tag-args-kwargs.py rename to docs/examples/python/template_tag_args_kwargs.py diff --git a/docs/examples/python/template-tag-bad-view.py b/docs/examples/python/template_tag_bad_view.py similarity index 100% rename from docs/examples/python/template-tag-bad-view.py rename to docs/examples/python/template_tag_bad_view.py diff --git a/docs/examples/python/todo_item_model.py b/docs/examples/python/todo_item_model.py new file mode 100644 index 00000000..ec98be92 --- /dev/null +++ b/docs/examples/python/todo_item_model.py @@ -0,0 +1,5 @@ +from django.db.models import CharField, Model + + +class TodoItem(Model): + text: CharField = CharField(max_length=255) diff --git a/docs/examples/python/use_auth.py b/docs/examples/python/use_auth.py new file mode 100644 index 00000000..2bb1bcbb --- /dev/null +++ b/docs/examples/python/use_auth.py @@ -0,0 +1,23 @@ +from django.contrib.auth import get_user_model +from reactpy import component, html + +from reactpy_django.hooks import use_auth, use_user + + +@component +def my_component(): + auth = use_auth() + user = use_user() + + async def login_user(event): + new_user, _created = await get_user_model().objects.aget_or_create(username="ExampleUser") + await auth.login(new_user) + + async def logout_user(event): + await auth.logout() + + return html.div( + f"Current User: {user}", + html.button({"onClick": login_user}, "Login"), + html.button({"onClick": logout_user}, "Logout"), + ) diff --git a/docs/examples/python/use-channel-layer.py b/docs/examples/python/use_channel_layer.py similarity index 66% rename from docs/examples/python/use-channel-layer.py rename to docs/examples/python/use_channel_layer.py index 83a66f19..ff9330a4 100644 --- a/docs/examples/python/use-channel-layer.py +++ b/docs/examples/python/use_channel_layer.py @@ -1,21 +1,22 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer @component def my_component(): async def receive_message(message): - set_message(message["text"]) + set_message_data(message["text"]) async def send_message(event): if event["key"] == "Enter": await sender({"text": event["target"]["value"]}) - message, set_message = hooks.use_state("") - sender = use_channel_layer("my-channel-name", receiver=receive_message) + message_data, set_message_data = hooks.use_state("") + sender = use_channel_layer(group="my-group-name", receiver=receive_message) return html.div( - f"Received: {message}", + f"Received: {message_data}", html.br(), "Send: ", html.input({"type": "text", "onKeyDown": send_message}), diff --git a/docs/examples/python/use-channel-layer-group.py b/docs/examples/python/use_channel_layer_group.py similarity index 72% rename from docs/examples/python/use-channel-layer-group.py rename to docs/examples/python/use_channel_layer_group.py index bcbabee6..4189b47c 100644 --- a/docs/examples/python/use-channel-layer-group.py +++ b/docs/examples/python/use_channel_layer_group.py @@ -1,10 +1,11 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer @component def my_sender_component(): - sender = use_channel_layer(group_name="my-group-name") + sender = use_channel_layer(group="my-group-name") async def submit_event(event): if event["key"] == "Enter": @@ -20,10 +21,10 @@ async def submit_event(event): def my_receiver_component_1(): message, set_message = hooks.use_state("") - async def receive_event(message): + async def receive_message(message): set_message(message["text"]) - use_channel_layer(group_name="my-group-name", receiver=receive_event) + use_channel_layer(group="my-group-name", receiver=receive_message) return html.div(f"Message Receiver 1: {message}") @@ -32,9 +33,9 @@ async def receive_event(message): def my_receiver_component_2(): message, set_message = hooks.use_state("") - async def receive_event(message): + async def receive_message(message): set_message(message["text"]) - use_channel_layer(group_name="my-group-name", receiver=receive_event) + use_channel_layer(group="my-group-name", receiver=receive_message) return html.div(f"Message Receiver 2: {message}") diff --git a/docs/examples/python/use-channel-layer-signal-receiver.py b/docs/examples/python/use_channel_layer_signal_receiver.py similarity index 54% rename from docs/examples/python/use-channel-layer-signal-receiver.py rename to docs/examples/python/use_channel_layer_signal_receiver.py index 57a92321..d858df3f 100644 --- a/docs/examples/python/use-channel-layer-signal-receiver.py +++ b/docs/examples/python/use_channel_layer_signal_receiver.py @@ -1,4 +1,5 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer @@ -6,9 +7,10 @@ def my_receiver_component(): message, set_message = hooks.use_state("") - async def receive_event(message): + async def receive_message(message): set_message(message["text"]) - use_channel_layer("my-channel-name", receiver=receive_event) + # This is defined to receive any messages from both "my-channel-name" and "my-group-name". + use_channel_layer(channel="my-channel-name", group="my-group-name", receiver=receive_message) return html.div(f"Message Receiver: {message}") diff --git a/docs/examples/python/use-channel-layer-signal-sender.py b/docs/examples/python/use_channel_layer_signal_sender.py similarity index 64% rename from docs/examples/python/use-channel-layer-signal-sender.py rename to docs/examples/python/use_channel_layer_signal_sender.py index a35a6c88..b900bd2c 100644 --- a/docs/examples/python/use-channel-layer-signal-sender.py +++ b/docs/examples/python/use_channel_layer_signal_sender.py @@ -12,8 +12,10 @@ class ExampleModel(Model): ... def my_sender_signal(sender, instance, **kwargs): layer = get_channel_layer() - # Example of sending a message to a channel - async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"}) - - # Example of sending a message to a group channel + # EXAMPLE 1: Sending a message to a group. + # Note that `group_send` requires using the `group` argument in `use_channel_layer`. async_to_sync(layer.group_send)("my-group-name", {"text": "Hello World!"}) + + # EXAMPLE 2: Sending a message to a single channel. + # Note that this is typically only used for channels that use point-to-point communication + async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"}) diff --git a/docs/examples/python/use_channel_layer_single.py b/docs/examples/python/use_channel_layer_single.py new file mode 100644 index 00000000..394cbc48 --- /dev/null +++ b/docs/examples/python/use_channel_layer_single.py @@ -0,0 +1,29 @@ +from reactpy import component, hooks, html + +from reactpy_django.hooks import use_channel_layer + + +@component +def my_sender_component(): + sender = use_channel_layer(channel="my-channel-name") + + async def submit_event(event): + if event["key"] == "Enter": + await sender({"text": event["target"]["value"]}) + + return html.div( + "Message Sender: ", + html.input({"type": "text", "onKeyDown": submit_event}), + ) + + +@component +def my_receiver_component(): + message, set_message = hooks.use_state("") + + async def receive_message(message): + set_message(message["text"]) + + use_channel_layer(channel="my-channel-name", receiver=receive_message) + + return html.div(f"Message Receiver 1: {message}") diff --git a/docs/examples/python/use-connection.py b/docs/examples/python/use_connection.py similarity index 99% rename from docs/examples/python/use-connection.py rename to docs/examples/python/use_connection.py index 1ea0fdb6..a15cd39b 100644 --- a/docs/examples/python/use-connection.py +++ b/docs/examples/python/use_connection.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_connection diff --git a/docs/examples/python/use-location.py b/docs/examples/python/use_location.py similarity index 99% rename from docs/examples/python/use-location.py rename to docs/examples/python/use_location.py index d7afcbac..454da7f6 100644 --- a/docs/examples/python/use-location.py +++ b/docs/examples/python/use_location.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_location diff --git a/docs/examples/python/use-mutation.py b/docs/examples/python/use_mutation.py similarity index 91% rename from docs/examples/python/use-mutation.py rename to docs/examples/python/use_mutation.py index 1bc69312..471a4a4c 100644 --- a/docs/examples/python/use-mutation.py +++ b/docs/examples/python/use_mutation.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_mutation @@ -24,6 +25,6 @@ def submit_event(event): return html.div( html.label("Add an item:"), - html.input({"type": "text", "on_key_down": submit_event}), + html.input({"type": "text", "onKeyDown": submit_event}), mutation_status, ) diff --git a/docs/examples/python/use-mutation-args-kwargs.py b/docs/examples/python/use_mutation_args_kwargs.py similarity index 71% rename from docs/examples/python/use-mutation-args-kwargs.py rename to docs/examples/python/use_mutation_args_kwargs.py index f9889777..9a4b1e0a 100644 --- a/docs/examples/python/use-mutation-args-kwargs.py +++ b/docs/examples/python/use_mutation_args_kwargs.py @@ -1,9 +1,9 @@ from reactpy import component + from reactpy_django.hooks import use_mutation -def example_mutation(value: int, other_value: bool = False): - ... +def example_mutation(value: int, other_value: bool = False): ... @component @@ -11,5 +11,3 @@ def my_component(): mutation = use_mutation(example_mutation) mutation(123, other_value=True) - - ... diff --git a/docs/examples/python/use-mutation-query-refetch.py b/docs/examples/python/use_mutation_query_refetch.py similarity index 85% rename from docs/examples/python/use-mutation-query-refetch.py rename to docs/examples/python/use_mutation_query_refetch.py index 227ab1a7..f2e014e9 100644 --- a/docs/examples/python/use-mutation-query-refetch.py +++ b/docs/examples/python/use_mutation_query_refetch.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_mutation, use_query @@ -26,9 +27,7 @@ def submit_event(event): elif item_query.error or not item_query.data: rendered_items = html.h2("Error when loading!") else: - rendered_items = html.ul( - html.li(item.text, key=item.pk) for item in item_query.data - ) + rendered_items = html.ul(html.li(item.text, key=item.pk) for item in item_query.data) # Handle all possible mutation states if item_mutation.loading: @@ -40,7 +39,7 @@ def submit_event(event): return html.div( html.label("Add an item:"), - html.input({"type": "text", "on_key_down": submit_event}), + html.input({"type": "text", "onKeyDown": submit_event}), mutation_status, rendered_items, ) diff --git a/docs/examples/python/use-mutation-reset.py b/docs/examples/python/use_mutation_reset.py similarity index 81% rename from docs/examples/python/use-mutation-reset.py rename to docs/examples/python/use_mutation_reset.py index 8eb1e042..486f0464 100644 --- a/docs/examples/python/use-mutation-reset.py +++ b/docs/examples/python/use_mutation_reset.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_mutation @@ -21,12 +22,12 @@ def submit_event(event): if item_mutation.loading: mutation_status = html.h2("Adding...") elif item_mutation.error: - mutation_status = html.button({"on_click": reset_event}, "Error: Try again!") + mutation_status = html.button({"onClick": reset_event}, "Error: Try again!") else: mutation_status = html.h2("Mutation done.") return html.div( html.label("Add an item:"), - html.input({"type": "text", "on_key_down": submit_event}), + html.input({"type": "text", "onKeyDown": submit_event}), mutation_status, ) diff --git a/docs/examples/python/use-mutation-thread-sensitive.py b/docs/examples/python/use_mutation_thread_sensitive.py similarity index 90% rename from docs/examples/python/use-mutation-thread-sensitive.py rename to docs/examples/python/use_mutation_thread_sensitive.py index 85046dc0..2d047696 100644 --- a/docs/examples/python/use-mutation-thread-sensitive.py +++ b/docs/examples/python/use_mutation_thread_sensitive.py @@ -1,10 +1,10 @@ from reactpy import component, html + from reactpy_django.hooks import use_mutation def execute_thread_safe_mutation(text): """This is an example mutation function that does some thread-safe operation.""" - pass @component @@ -26,6 +26,6 @@ def submit_event(event): mutation_status = html.h2("Done.") return html.div( - html.input({"type": "text", "on_key_down": submit_event}), + html.input({"type": "text", "onKeyDown": submit_event}), mutation_status, ) diff --git a/docs/examples/python/use-origin.py b/docs/examples/python/use_origin.py similarity index 99% rename from docs/examples/python/use-origin.py rename to docs/examples/python/use_origin.py index e8763bbf..f0713db9 100644 --- a/docs/examples/python/use-origin.py +++ b/docs/examples/python/use_origin.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_origin diff --git a/docs/examples/python/use-query.py b/docs/examples/python/use_query.py similarity index 82% rename from docs/examples/python/use-query.py rename to docs/examples/python/use_query.py index 5688765b..9cadbd25 100644 --- a/docs/examples/python/use-query.py +++ b/docs/examples/python/use_query.py @@ -1,6 +1,7 @@ from channels.db import database_sync_to_async -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_query @@ -17,8 +18,6 @@ def todo_list(): elif item_query.error or not item_query.data: rendered_items = html.h2("Error when loading!") else: - rendered_items = html.ul( - [html.li(item.text, key=item.pk) for item in item_query.data] - ) + rendered_items = html.ul([html.li(item.text, key=item.pk) for item in item_query.data]) return html.div("Rendered items: ", rendered_items) diff --git a/docs/examples/python/use-query-args.py b/docs/examples/python/use_query_args.py similarity index 99% rename from docs/examples/python/use-query-args.py rename to docs/examples/python/use_query_args.py index 8deb549a..a37f7277 100644 --- a/docs/examples/python/use-query-args.py +++ b/docs/examples/python/use_query_args.py @@ -1,4 +1,5 @@ from reactpy import component + from reactpy_django.hooks import use_query diff --git a/docs/examples/python/use-query-postprocessor-change.py b/docs/examples/python/use_query_postprocessor_change.py similarity index 98% rename from docs/examples/python/use-query-postprocessor-change.py rename to docs/examples/python/use_query_postprocessor_change.py index 5685956d..2faba050 100644 --- a/docs/examples/python/use-query-postprocessor-change.py +++ b/docs/examples/python/use_query_postprocessor_change.py @@ -1,4 +1,5 @@ from reactpy import component + from reactpy_django.hooks import use_query @@ -11,7 +12,6 @@ def my_postprocessor(data, example_kwarg=True): def execute_io_intensive_operation(): """This is an example query function that does something IO intensive.""" - pass @component diff --git a/docs/examples/python/use-query-postprocessor-disable.py b/docs/examples/python/use_query_postprocessor_disable.py similarity index 97% rename from docs/examples/python/use-query-postprocessor-disable.py rename to docs/examples/python/use_query_postprocessor_disable.py index e9541924..a22f7a96 100644 --- a/docs/examples/python/use-query-postprocessor-disable.py +++ b/docs/examples/python/use_query_postprocessor_disable.py @@ -1,10 +1,10 @@ from reactpy import component + from reactpy_django.hooks import use_query def execute_io_intensive_operation(): """This is an example query function that does something IO intensive.""" - pass @component diff --git a/docs/examples/python/use-query-postprocessor-kwargs.py b/docs/examples/python/use_query_postprocessor_kwargs.py similarity index 99% rename from docs/examples/python/use-query-postprocessor-kwargs.py rename to docs/examples/python/use_query_postprocessor_kwargs.py index 4ed108af..18ba2999 100644 --- a/docs/examples/python/use-query-postprocessor-kwargs.py +++ b/docs/examples/python/use_query_postprocessor_kwargs.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component + +from example.models import TodoItem from reactpy_django.hooks import use_query diff --git a/docs/examples/python/use_query_refetch.py b/docs/examples/python/use_query_refetch.py new file mode 100644 index 00000000..c31c12c7 --- /dev/null +++ b/docs/examples/python/use_query_refetch.py @@ -0,0 +1,26 @@ +from channels.db import database_sync_to_async +from reactpy import component, html + +from example.models import TodoItem +from reactpy_django.hooks import use_query + + +async def get_items(): + return await database_sync_to_async(TodoItem.objects.all)() + + +@component +def todo_list(): + item_query = use_query(get_items) + + if item_query.loading: + rendered_items = html.h2("Loading...") + elif item_query.error or not item_query.data: + rendered_items = html.h2("Error when loading!") + else: + rendered_items = html.ul([html.li(item.text, key=item.pk) for item in item_query.data]) + + def on_click(event): + item_query.refetch() + + return html.div("Rendered items: ", rendered_items, html.button({"onClick": on_click}, "Refresh")) diff --git a/docs/examples/python/use-query-thread-sensitive.py b/docs/examples/python/use_query_thread_sensitive.py similarity index 97% rename from docs/examples/python/use-query-thread-sensitive.py rename to docs/examples/python/use_query_thread_sensitive.py index d657be6b..9b929e3a 100644 --- a/docs/examples/python/use-query-thread-sensitive.py +++ b/docs/examples/python/use_query_thread_sensitive.py @@ -1,10 +1,10 @@ from reactpy import component + from reactpy_django.hooks import use_query def execute_thread_safe_operation(): """This is an example query function that does some thread-safe operation.""" - pass @component diff --git a/docs/examples/python/use_rerender.py b/docs/examples/python/use_rerender.py new file mode 100644 index 00000000..cd160e17 --- /dev/null +++ b/docs/examples/python/use_rerender.py @@ -0,0 +1,15 @@ +from uuid import uuid4 + +from reactpy import component, html + +from reactpy_django.hooks import use_rerender + + +@component +def my_component(): + rerender = use_rerender() + + def on_click(): + rerender() + + return html.div(f"UUID: {uuid4()}", html.button({"onClick": on_click}, "Rerender")) diff --git a/docs/examples/python/use-root-id.py b/docs/examples/python/use_root_id.py similarity index 99% rename from docs/examples/python/use-root-id.py rename to docs/examples/python/use_root_id.py index f2088cc4..246a8da1 100644 --- a/docs/examples/python/use-root-id.py +++ b/docs/examples/python/use_root_id.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_root_id diff --git a/docs/examples/python/use-scope.py b/docs/examples/python/use_scope.py similarity index 99% rename from docs/examples/python/use-scope.py rename to docs/examples/python/use_scope.py index 2e6f5961..2bd8f483 100644 --- a/docs/examples/python/use-scope.py +++ b/docs/examples/python/use_scope.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_scope diff --git a/docs/examples/python/use-user.py b/docs/examples/python/use_user.py similarity index 99% rename from docs/examples/python/use-user.py rename to docs/examples/python/use_user.py index 641bbeee..597e9f67 100644 --- a/docs/examples/python/use-user.py +++ b/docs/examples/python/use_user.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_user diff --git a/docs/examples/python/use-user-data.py b/docs/examples/python/use_user_data.py similarity index 91% rename from docs/examples/python/use-user-data.py rename to docs/examples/python/use_user_data.py index bc0ffaff..1526d6a8 100644 --- a/docs/examples/python/use-user-data.py +++ b/docs/examples/python/use_user_data.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_user_data @@ -15,5 +16,5 @@ def on_submit(event): html.div(f"Data: {query.data}"), html.div(f"Loading: {query.loading | mutation.loading}"), html.div(f"Error(s): {query.error} {mutation.error}"), - html.input({"on_key_press": on_submit}), + html.input({"onKeyPress": on_submit}), ) diff --git a/docs/examples/python/use-user-data-defaults.py b/docs/examples/python/use_user_data_defaults.py similarity index 99% rename from docs/examples/python/use-user-data-defaults.py rename to docs/examples/python/use_user_data_defaults.py index 7a1380bc..2c066ad7 100644 --- a/docs/examples/python/use-user-data-defaults.py +++ b/docs/examples/python/use_user_data_defaults.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_user_data diff --git a/docs/examples/python/user-passes-test.py b/docs/examples/python/user_passes_test.py similarity index 99% rename from docs/examples/python/user-passes-test.py rename to docs/examples/python/user_passes_test.py index 201ad831..37160c1b 100644 --- a/docs/examples/python/user-passes-test.py +++ b/docs/examples/python/user_passes_test.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.decorators import user_passes_test diff --git a/docs/examples/python/user-passes-test-component-fallback.py b/docs/examples/python/user_passes_test_component_fallback.py similarity index 99% rename from docs/examples/python/user-passes-test-component-fallback.py rename to docs/examples/python/user_passes_test_component_fallback.py index 9fb71ea7..b18330d1 100644 --- a/docs/examples/python/user-passes-test-component-fallback.py +++ b/docs/examples/python/user_passes_test_component_fallback.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.decorators import user_passes_test diff --git a/docs/examples/python/user-passes-test-vdom-fallback.py b/docs/examples/python/user_passes_test_vdom_fallback.py similarity index 99% rename from docs/examples/python/user-passes-test-vdom-fallback.py rename to docs/examples/python/user_passes_test_vdom_fallback.py index 5d5c54f4..9dd1ad65 100644 --- a/docs/examples/python/user-passes-test-vdom-fallback.py +++ b/docs/examples/python/user_passes_test_vdom_fallback.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.decorators import user_passes_test diff --git a/docs/examples/python/views.py b/docs/examples/python/views.py deleted file mode 100644 index 60ebc945..00000000 --- a/docs/examples/python/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from .hello_world_cbv import HelloWorld -from .hello_world_fbv import hello_world - -__all__ = [ - "HelloWorld", - "hello_world", -] diff --git a/docs/examples/python/vtc.py b/docs/examples/python/vtc.py index 194d35cc..84c7aeb2 100644 --- a/docs/examples/python/vtc.py +++ b/docs/examples/python/vtc.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.hello_world) diff --git a/docs/examples/python/vtc-args.py b/docs/examples/python/vtc_args.py similarity index 95% rename from docs/examples/python/vtc-args.py rename to docs/examples/python/vtc_args.py index edc0fbb2..9ce081b5 100644 --- a/docs/examples/python/vtc-args.py +++ b/docs/examples/python/vtc_args.py @@ -1,8 +1,8 @@ from django.http import HttpRequest from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.hello_world) diff --git a/docs/examples/python/vtc-cbv.py b/docs/examples/python/vtc_cbv.py similarity index 90% rename from docs/examples/python/vtc-cbv.py rename to docs/examples/python/vtc_cbv.py index 47509b75..38e40efe 100644 --- a/docs/examples/python/vtc-cbv.py +++ b/docs/examples/python/vtc_cbv.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.HelloWorld.as_view()) diff --git a/docs/examples/python/vtc-strict-parsing.py b/docs/examples/python/vtc_strict_parsing.py similarity index 90% rename from docs/examples/python/vtc-strict-parsing.py rename to docs/examples/python/vtc_strict_parsing.py index 194d35cc..84c7aeb2 100644 --- a/docs/examples/python/vtc-strict-parsing.py +++ b/docs/examples/python/vtc_strict_parsing.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.hello_world) diff --git a/docs/examples/python/vtc-transforms.py b/docs/examples/python/vtc_transforms.py similarity index 75% rename from docs/examples/python/vtc-transforms.py rename to docs/examples/python/vtc_transforms.py index adbf9ea1..b8402481 100644 --- a/docs/examples/python/vtc-transforms.py +++ b/docs/examples/python/vtc_transforms.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component def example_transform(vdom): @@ -10,9 +10,7 @@ def example_transform(vdom): vdom["children"][0] = "Farewell World!" -hello_world_component = view_to_component( - views.hello_world, transforms=[example_transform] -) +hello_world_component = view_to_component(views.hello_world, transforms=[example_transform]) @component diff --git a/docs/examples/python/vti.py b/docs/examples/python/vti.py index c8ff6796..207e5bc5 100644 --- a/docs/examples/python/vti.py +++ b/docs/examples/python/vti.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe hello_world_iframe = view_to_iframe(views.hello_world) diff --git a/docs/examples/python/vti-args.py b/docs/examples/python/vti_args.py similarity index 93% rename from docs/examples/python/vti-args.py rename to docs/examples/python/vti_args.py index f5013ecd..a26c3d3a 100644 --- a/docs/examples/python/vti-args.py +++ b/docs/examples/python/vti_args.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe hello_world_iframe = view_to_iframe( views.hello_world, diff --git a/docs/examples/python/vti-cbv.py b/docs/examples/python/vti_cbv.py similarity index 90% rename from docs/examples/python/vti-cbv.py rename to docs/examples/python/vti_cbv.py index 4e1f1b44..63f182ae 100644 --- a/docs/examples/python/vti-cbv.py +++ b/docs/examples/python/vti_cbv.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe hello_world_iframe = view_to_iframe(views.HelloWorld.as_view()) diff --git a/docs/examples/python/vti-extra-props.py b/docs/examples/python/vti_extra_props.py similarity index 60% rename from docs/examples/python/vti-extra-props.py rename to docs/examples/python/vti_extra_props.py index 655ad541..09846a1c 100644 --- a/docs/examples/python/vti-extra-props.py +++ b/docs/examples/python/vti_extra_props.py @@ -1,11 +1,9 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe -hello_world_iframe = view_to_iframe( - views.hello_world, extra_props={"title": "Hello World!"} -) +hello_world_iframe = view_to_iframe(views.hello_world, extra_props={"title": "Hello World!"}) @component diff --git a/docs/includes/auth-middleware-stack.md b/docs/includes/auth-middleware-stack.md new file mode 100644 index 00000000..7cc0c7f8 --- /dev/null +++ b/docs/includes/auth-middleware-stack.md @@ -0,0 +1,3 @@ +```python linenums="0" +{% include "../examples/python/configure_asgi_middleware.py" start="# start" %} +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e4159640..bde5ac08 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -16,9 +16,7 @@ nav: - Management Commands: reference/management-commands.md - About: - Changelog: about/changelog.md - - Contributor Guide: - - Code: about/code.md - - Docs: about/docs.md + - Contributor Guide: about/contributing.md - Community: - GitHub Discussions: https://github.com/reactive-python/reactpy-django/discussions - Discord: https://discord.gg/uNb5P4hA9X @@ -89,8 +87,6 @@ plugins: - spellcheck: known_words: dictionary.txt allow_unicode: no - ignore_code: yes - # - section-index extra: generator: false @@ -128,6 +124,6 @@ site_description: It's React, but in Python. Now with Django integration. copyright: '©

Reactive Python and affiliates. ' repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django -repo_name: ReactPy Django (GitHub) +repo_name: ReactPy Django edit_uri: edit/main/docs/src docs_dir: src diff --git a/docs/overrides/home-code-examples/create-user-interfaces.py b/docs/overrides/home-code-examples/create-user-interfaces.py deleted file mode 100644 index 37776ab1..00000000 --- a/docs/overrides/home-code-examples/create-user-interfaces.py +++ /dev/null @@ -1,22 +0,0 @@ -from reactpy import component, html - - -def thumbnail(video): - return None - - -def like_button(video): - return None - - -@component -def video(video): - return html.div( - thumbnail(video), - html.a( - {"href": video.url}, - html.h3(video.title), - html.p(video.description), - ), - like_button(video), - ) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 67e31441..93d5ca29 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -73,9 +73,9 @@

Create user interfaces from components

{% with image="create-user-interfaces.png", class="pop-left" %} - {% include "home-code-examples/code-block.html" %} + {% include "homepage_examples/code_block.html" %} {% endwith %} - {% include "home-code-examples/create-user-interfaces-demo.html" %} + {% include "homepage_examples/create_user_interfaces_demo.html" %}

Whether you work on your own or with thousands of other developers, using React feels the same. It is @@ -94,9 +94,9 @@

Write components with pure Python code

{% with image="write-components-with-python.png", class="pop-left" %} - {% include "home-code-examples/code-block.html" %} + {% include "homepage_examples/code_block.html" %} {% endwith %} - {% include "home-code-examples/write-components-with-python-demo.html" %} + {% include "homepage_examples/write_components_with_python_demo.html" %}
@@ -110,9 +110,9 @@

Add interactivity wherever you need it

{% with image="add-interactivity.png" %} - {% include "home-code-examples/code-block.html" %} + {% include "homepage_examples/code_block.html" %} {% endwith %} - {% include "home-code-examples/add-interactivity-demo.html" %} + {% include "homepage_examples/add_interactivity_demo.html" %}

You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/homepage_examples/add_interactivity.py similarity index 64% rename from docs/overrides/home-code-examples/add-interactivity.py rename to docs/overrides/homepage_examples/add_interactivity.py index 90976446..9a7bf76f 100644 --- a/docs/overrides/home-code-examples/add-interactivity.py +++ b/docs/overrides/homepage_examples/add_interactivity.py @@ -1,16 +1,15 @@ +# ruff: noqa: INP001 from reactpy import component, html, use_state -def filter_videos(videos, search_text): - return None +def filter_videos(*_, **__): + return [] -def search_input(dictionary, value): - return None +def search_input(*_, **__): ... -def video_list(videos, empty_heading): - return None +def video_list(*_, **__): ... @component @@ -20,7 +19,7 @@ def searchable_video_list(videos): return html._( search_input( - {"on_change": lambda new_text: set_search_text(new_text)}, + {"onChange": lambda event: set_search_text(event["target"]["value"])}, value=search_text, ), video_list( diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/homepage_examples/add_interactivity_demo.html similarity index 100% rename from docs/overrides/home-code-examples/add-interactivity-demo.html rename to docs/overrides/homepage_examples/add_interactivity_demo.html diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/homepage_examples/code_block.html similarity index 100% rename from docs/overrides/home-code-examples/code-block.html rename to docs/overrides/homepage_examples/code_block.html diff --git a/docs/overrides/homepage_examples/create_user_interfaces.py b/docs/overrides/homepage_examples/create_user_interfaces.py new file mode 100644 index 00000000..7878aa6b --- /dev/null +++ b/docs/overrides/homepage_examples/create_user_interfaces.py @@ -0,0 +1,21 @@ +# ruff: noqa: INP001 +from reactpy import component, html + + +def thumbnail(*_, **__): ... + + +def like_button(*_, **__): ... + + +@component +def video(data): + return html.div( + thumbnail(data), + html.a( + {"href": data.url}, + html.h3(data.title), + html.p(data.description), + ), + like_button(data), + ) diff --git a/docs/overrides/home-code-examples/create-user-interfaces-demo.html b/docs/overrides/homepage_examples/create_user_interfaces_demo.html similarity index 100% rename from docs/overrides/home-code-examples/create-user-interfaces-demo.html rename to docs/overrides/homepage_examples/create_user_interfaces_demo.html diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/homepage_examples/write_components_with_python.py similarity index 76% rename from docs/overrides/home-code-examples/write-components-with-python.py rename to docs/overrides/homepage_examples/write_components_with_python.py index 6af43baa..5993046c 100644 --- a/docs/overrides/home-code-examples/write-components-with-python.py +++ b/docs/overrides/homepage_examples/write_components_with_python.py @@ -1,6 +1,10 @@ +# ruff: noqa: INP001 from reactpy import component, html +def video(*_, **__): ... + + @component def video_list(videos, empty_heading): count = len(videos) @@ -11,5 +15,5 @@ def video_list(videos, empty_heading): return html.section( html.h2(heading), - [video(video) for video in videos], + [video(x, key=x.id) for x in videos], ) diff --git a/docs/overrides/home-code-examples/write-components-with-python-demo.html b/docs/overrides/homepage_examples/write_components_with_python_demo.html similarity index 100% rename from docs/overrides/home-code-examples/write-components-with-python-demo.html rename to docs/overrides/homepage_examples/write_components_with_python_demo.html diff --git a/docs/src/about/code.md b/docs/src/about/code.md deleted file mode 100644 index 205c2c96..00000000 --- a/docs/src/about/code.md +++ /dev/null @@ -1,84 +0,0 @@ -## Overview - -

- - You will need to set up a Python environment to develop ReactPy-Django. - -

- -!!! abstract "Note" - - Looking to contribute features that are not Django specific? - - Everything within the `reactpy-django` repository must be specific to Django integration. Check out the [ReactPy Core documentation](https://reactpy.dev/docs/about/contributor-guide.html) to contribute general features such as components, hooks, and events. - ---- - -## Creating an environment - -If you plan to make code changes to this repository, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -``` - -Then, by running the command below you can install the dependencies needed to run the ReactPy-Django development environment. - -```bash linenums="0" -pip install -r requirements.txt --upgrade --verbose -``` - -!!! warning "Pitfall" - - Some of our development dependencies require a C++ compiler, which is not installed by default on Windows. If you receive errors related to this during installation, follow the instructions in your console errors. - - Additionally, be aware that ReactPy-Django's JavaScript bundle is built within the following scenarios: - - 1. When `pip install` is run on the `reactpy-django` package. - 2. Every time `python manage.py ...` or `nox ...` is run - -## Running the full test suite - -!!! abstract "Note" - - This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. - -By running the command below you can run the full test suite: - -```bash linenums="0" -nox -t test -``` - -Or, if you want to run the tests in the background: - -```bash linenums="0" -nox -t test -- --headless -``` - -## Running Django tests - -If you want to only run our Django tests in your current environment, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py test -``` - -## Running Django test web server - -If you want to manually run the Django test application, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py runserver -``` - -## Creating a pull request - -{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md new file mode 100644 index 00000000..06499c3e --- /dev/null +++ b/docs/src/about/contributing.md @@ -0,0 +1,94 @@ +## Overview + +

+ + You will need to set up a Python environment to develop ReactPy-Django. + +

+ +!!! abstract "Note" + + Looking to contribute features that are not Django specific? + + Everything within the `reactpy-django` repository must be specific to Django integration. Check out the [ReactPy Core documentation](https://reactpy.dev/docs/about/contributor-guide.html) to contribute general features such as components, hooks, and events. + +--- + +## Creating a development environment + +If you plan to make code changes to this repository, you will need to install the following dependencies first: + +- [Git](https://git-scm.com/downloads) +- [Python 3.9+](https://www.python.org/downloads/) +- [Hatch](https://hatch.pypa.io/latest/) +- [Bun](https://bun.sh/) + +Once you finish installing these dependencies, you can clone this repository: + +```bash linenums="0" +git clone https://github.com/reactive-python/reactpy-django.git +cd reactpy-django +``` + +## Executing test environment commands + +By utilizing `hatch`, the following commands are available to manage the development environment. + +### Tests + +| Command | Description | +| --- | --- | +| `hatch test` | Run Python tests using the current environment's Python version | +| `hatch test --all` | Run tests using all compatible Python versions | +| `hatch test --python 3.9` | Run tests using a specific Python version | +| `hatch test --include "django=5.1"` | Run tests using a specific Django version | +| `hatch test -k test_object_in_templatetag` | Run only a specific test | +| `hatch test --ds test_app.settings_multi_db` | Run tests with a specific Django settings file | +| `hatch run django:runserver` | Manually run the Django development server without running tests | + +??? question "What other arguments are available to me?" + + The `hatch test` command is a wrapper for `pytest`. Hatch "intercepts" a handful of arguments, which can be previewed by typing `hatch test --help`. + + Any additional arguments in the `test` command are provided directly to pytest. See the [pytest documentation](https://docs.pytest.org/en/stable/reference/reference.html#command-line-flags) for what additional arguments are available. + +### Linting and Formatting + +| Command | Description | +| --- | --- | +| `hatch fmt` | Run all linters and formatters | +| `hatch fmt --check` | Run all linters and formatters, but do not save fixes to the disk | +| `hatch fmt --linter` | Run only linters | +| `hatch fmt --formatter` | Run only formatters | +| `hatch run javascript:check` | Run the JavaScript linter/formatter | +| `hatch run javascript:fix` | Run the JavaScript linter/formatter and write fixes to disk | +| `hatch run python:type_check` | Run the Python type checker | + +??? tip "Configure your IDE for linting" + + This repository uses `hatch fmt` for linting and formatting, which is a [modestly customized](https://hatch.pypa.io/latest/config/internal/static-analysis/#default-settings) version of [`ruff`](https://github.com/astral-sh/ruff). + + You can install `ruff` as a plugin to your preferred code editor to create a similar environment. + +### Documentation + +| Command | Description | +| --- | --- | +| `hatch run docs:serve` | Start the [`mkdocs`](https://www.mkdocs.org/) server to view documentation locally | +| `hatch run docs:build` | Build the documentation | +| `hatch run docs:linkcheck` | Check for broken links in the documentation | +| `hatch fmt docs --check` | Run linter on code examples in the documentation | + +### Environment Management + +| Command | Description | +| --- | --- | +| `hatch build --clean` | Build the package from source | +| `hatch env prune` | Delete all virtual environments created by `hatch` | +| `hatch python install 3.12` | Install a specific Python version to your system | + +??? tip "Check out Hatch for all available commands!" + + This documentation only covers commonly used commands. + + You can type `hatch --help` to see all available commands. diff --git a/docs/src/about/docs.md b/docs/src/about/docs.md deleted file mode 100644 index 712570ec..00000000 --- a/docs/src/about/docs.md +++ /dev/null @@ -1,45 +0,0 @@ -## Overview - -

- -You will need to set up a Python environment to create, test, and preview docs changes. - -

- ---- - -## Modifying Docs - -If you plan to make changes to this documentation, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -``` - -Then, by running the command below you can: - -- Install an editable version of the documentation -- Self-host a test server for the documentation - -```bash linenums="0" -pip install -r requirements.txt --upgrade -``` - -Finally, to verify that everything is working properly, you can manually run the docs preview web server. - -```bash linenums="0" -cd docs -mkdocs serve -``` - -Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to view a preview of the documentation. - -## GitHub Pull Request - -{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index c72e7093..70f05cf2 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -1,335 +1,337 @@ img.home-logo { - height: 120px; + height: 120px; } .home .row { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - padding: 6rem 0.8rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 6rem 0.8rem; } .home .row:not(.first, .stripe) { - background: var(--row-bg-color); + background: var(--row-bg-color); } .home .row.stripe { - background: var(--row-stripe-bg-color); - border: 0 solid var(--stripe-border-color); - border-top-width: 1px; - border-bottom-width: 1px; + background: var(--row-stripe-bg-color); + border: 0 solid var(--stripe-border-color); + border-top-width: 1px; + border-bottom-width: 1px; } .home .row.first { - text-align: center; + text-align: center; } .home .row h1 { - max-width: 28rem; - line-height: 1.15; - font-weight: 500; - margin-bottom: 0.55rem; - margin-top: -1rem; + max-width: 28rem; + line-height: 1.15; + font-weight: 500; + margin-bottom: 0.55rem; + margin-top: -1rem; } .home .row.first h1 { - margin-top: 0.55rem; - margin-bottom: -0.75rem; + margin-top: 0.55rem; + margin-bottom: -0.75rem; } .home .row > p { - max-width: 35rem; - line-height: 1.5; - font-weight: 400; + max-width: 35rem; + line-height: 1.5; + font-weight: 400; } .home .row.first > p { - font-size: 32px; - font-weight: 500; + font-size: 32px; + font-weight: 500; } /* Code blocks */ .home .row .tabbed-set { - background: var(--home-tabbed-set-bg-color); - margin: 0; + background: var(--home-tabbed-set-bg-color); + margin: 0; } .home .row .tabbed-content { - padding: 20px 18px; - overflow-x: auto; + padding: 20px 18px; + overflow-x: auto; } .home .row .tabbed-content img { - user-select: none; - -moz-user-select: none; - -webkit-user-drag: none; - -webkit-user-select: none; - -ms-user-select: none; - max-width: 580px; + user-select: none; + -moz-user-select: none; + -webkit-user-drag: none; + -webkit-user-select: none; + -ms-user-select: none; + max-width: 580px; } .home .row .tabbed-content { - -webkit-filter: var(--code-block-filter); - filter: var(--code-block-filter); + -webkit-filter: var(--code-block-filter); + filter: var(--code-block-filter); } /* Code examples */ .home .example-container { - background: radial-gradient( - circle at 0% 100%, - rgb(41 84 147 / 11%) 0%, - rgb(22 89 189 / 4%) 70%, - rgb(48 99 175 / 0%) 80% - ), - radial-gradient( - circle at 100% 100%, - rgb(24 87 45 / 55%) 0%, - rgb(29 61 12 / 4%) 70%, - rgb(94 116 93 / 0%) 80% - ), - radial-gradient( - circle at 100% 0%, - rgba(54, 66, 84, 0.55) 0%, - rgb(102 111 125 / 4%) 70%, - rgba(54, 66, 84, 0) 80% - ), - radial-gradient( - circle at 0% 0%, - rgba(91, 114, 135, 0.55) 0%, - rgb(45 111 171 / 4%) 70%, - rgb(5 82 153 / 0%) 80% - ), - rgb(0, 0, 0) center center/cover no-repeat fixed; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - align-items: center; - border-radius: 16px; - margin: 30px 0; - max-width: 100%; - grid-column-gap: 20px; - padding-left: 20px; - padding-right: 20px; + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 11%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 87 45 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: center; + border-radius: 16px; + margin: 30px 0; + max-width: 100%; + grid-column-gap: 20px; + padding-left: 20px; + padding-right: 20px; } .home .demo .white-bg { - background: #fff; - border-radius: 16px; - display: flex; - flex-direction: column; - max-width: 590px; - min-width: -webkit-min-content; - min-width: -moz-min-content; - min-width: min-content; - row-gap: 1rem; - padding: 1rem; + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + max-width: 590px; + min-width: -webkit-min-content; + min-width: -moz-min-content; + min-width: min-content; + row-gap: 1rem; + padding: 1rem; + border: 1px rgb(0 0 0 / 20%) solid; + overflow: hidden; } .home .demo .vid-row { - display: flex; - flex-direction: row; - -moz-column-gap: 12px; - column-gap: 12px; + display: flex; + flex-direction: row; + -moz-column-gap: 12px; + column-gap: 12px; } .home .demo { - color: #000; + color: #000; } .home .demo .vid-thumbnail { - background: radial-gradient( - circle at 0% 100%, - rgb(41 84 147 / 55%) 0%, - rgb(22 89 189 / 4%) 70%, - rgb(48 99 175 / 0%) 80% - ), - radial-gradient( - circle at 100% 100%, - rgb(24 63 87 / 55%) 0%, - rgb(29 61 12 / 4%) 70%, - rgb(94 116 93 / 0%) 80% - ), - radial-gradient( - circle at 100% 0%, - rgba(54, 66, 84, 0.55) 0%, - rgb(102 111 125 / 4%) 70%, - rgba(54, 66, 84, 0) 80% - ), - radial-gradient( - circle at 0% 0%, - rgba(91, 114, 135, 0.55) 0%, - rgb(45 111 171 / 4%) 70%, - rgb(5 82 153 / 0%) 80% - ), - rgb(0, 0, 0) center center/cover no-repeat fixed; - width: 9rem; - aspect-ratio: 16 / 9; - border-radius: 8px; - display: flex; - justify-content: center; - align-items: center; + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 55%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 63 87 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + width: 9rem; + aspect-ratio: 16 / 9; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; } .home .demo .vid-text { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 100%; } .home .demo h2 { - font-size: 18px; - line-height: 1.375; - margin: 0; - text-align: left; - font-weight: 700; + font-size: 18px; + line-height: 1.375; + margin: 0; + text-align: left; + font-weight: 700; } .home .demo h3 { - font-size: 16px; - line-height: 1.25; - margin: 0; + font-size: 16px; + line-height: 1.25; + margin: 0; } .home .demo p { - font-size: 14px; - line-height: 1.375; - margin: 0; + font-size: 14px; + line-height: 1.375; + margin: 0; } .home .demo .browser-nav-url { - background: rgba(153, 161, 179, 0.2); - border-radius: 9999px; - font-size: 14px; - color: grey; - display: flex; - align-items: center; - justify-content: center; - -moz-column-gap: 5px; - column-gap: 5px; + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + font-size: 14px; + color: grey; + display: flex; + align-items: center; + justify-content: center; + -moz-column-gap: 5px; + column-gap: 5px; } .home .demo .browser-navbar { - margin: -1rem; - margin-bottom: 0; - padding: 0.75rem 1rem; - border-bottom: 1px solid darkgrey; + margin: -1rem; + margin-bottom: 0; + padding: 0.75rem 1rem; + border-bottom: 1px solid darkgrey; } .home .demo .browser-viewport { - background: #fff; - border-radius: 16px; - display: flex; - flex-direction: column; - row-gap: 1rem; - height: 400px; - overflow-y: scroll; - margin: -1rem; - padding: 1rem; + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + row-gap: 1rem; + height: 400px; + overflow-y: scroll; + margin: -1rem; + padding: 1rem; } .home .demo .browser-viewport .search-header > h1 { - color: #000; - text-align: left; - font-size: 24px; - margin: 0; + color: #000; + text-align: left; + font-size: 24px; + margin: 0; } .home .demo .browser-viewport .search-header > p { - text-align: left; - font-size: 16px; - margin: 10px 0; + text-align: left; + font-size: 16px; + margin: 10px 0; } .home .demo .search-bar input { - width: 100%; - background: rgba(153, 161, 179, 0.2); - border-radius: 9999px; - padding-left: 40px; - padding-right: 40px; - height: 40px; - color: #000; + width: 100%; + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + padding-left: 40px; + padding-right: 40px; + height: 40px; + color: #000; } .home .demo .search-bar svg { - height: 40px; - position: absolute; - transform: translateX(75%); + height: 40px; + position: absolute; + transform: translateX(75%); } .home .demo .search-bar { - position: relative; + position: relative; } /* Desktop Styling */ @media screen and (min-width: 60em) { - .home .row { - text-align: center; - } - .home .row > p { - font-size: 21px; - } - .home .row > h1 { - font-size: 52px; - } - .home .row .pop-left { - margin-left: -20px; - margin-right: 0; - margin-top: -20px; - margin-bottom: -20px; - } - .home .row .pop-right { - margin-left: 0px; - margin-right: 0px; - margin-top: -20px; - margin-bottom: -20px; - } + .home .row { + text-align: center; + } + .home .row > p { + font-size: 21px; + } + .home .row > h1 { + font-size: 52px; + } + .home .row .pop-left { + margin-left: -20px; + margin-right: 0; + margin-top: -20px; + margin-bottom: -20px; + } + .home .row .pop-right { + margin-left: 0px; + margin-right: 0px; + margin-top: -20px; + margin-bottom: -20px; + } } /* Mobile Styling */ @media screen and (max-width: 60em) { - .home .row { - padding: 4rem 0.8rem; - } - .home .row > h1, - .home .row > p { - padding-left: 1rem; - padding-right: 1rem; - } - .home .row.first { - padding-top: 2rem; - } - .home-btns { - width: 100%; - display: grid; - grid-gap: 0.5rem; - gap: 0.5rem; - } - .home .example-container { - display: flex; - flex-direction: column; - row-gap: 20px; - width: 100%; - justify-content: center; - border-radius: 0; - padding: 1rem 0; - } - .home .row { - padding-left: 0; - padding-right: 0; - } - .home .tabbed-set { - width: 100%; - border-radius: 0; - } - .home .demo { - width: 100%; - display: flex; - justify-content: center; - } - .home .demo > .white-bg { - width: 80%; - max-width: 80%; - } + .home .row { + padding: 4rem 0.8rem; + } + .home .row > h1, + .home .row > p { + padding-left: 1rem; + padding-right: 1rem; + } + .home .row.first { + padding-top: 2rem; + } + .home-btns { + width: 100%; + display: grid; + grid-gap: 0.5rem; + gap: 0.5rem; + } + .home .example-container { + display: flex; + flex-direction: column; + row-gap: 20px; + width: 100%; + justify-content: center; + border-radius: 0; + padding: 1rem 0; + } + .home .row { + padding-left: 0; + padding-right: 0; + } + .home .tabbed-set { + width: 100%; + border-radius: 0; + } + .home .demo { + width: 100%; + display: flex; + justify-content: center; + } + .home .demo > .white-bg { + width: 80%; + max-width: 80%; + } } diff --git a/docs/src/assets/img/add-interactivity.png b/docs/src/assets/img/add-interactivity.png index e5e24d29..c3243190 100644 Binary files a/docs/src/assets/img/add-interactivity.png and b/docs/src/assets/img/add-interactivity.png differ diff --git a/docs/src/assets/img/create-user-interfaces.png b/docs/src/assets/img/create-user-interfaces.png index 13abd064..06f6ea0c 100644 Binary files a/docs/src/assets/img/create-user-interfaces.png and b/docs/src/assets/img/create-user-interfaces.png differ diff --git a/docs/src/assets/img/write-components-with-python.png b/docs/src/assets/img/write-components-with-python.png index ba34cdf9..380d2c3a 100644 Binary files a/docs/src/assets/img/write-components-with-python.png and b/docs/src/assets/img/write-components-with-python.png differ diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 66265e78..d2ff722d 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -1,43 +1,51 @@ +asgi +async +backend +backends +backhaul +broadcasted +changelog django -sanic -plotly +frontend +frontends +hello_world +html +iframe +jupyter +keyworded +middleware +misconfiguration +misconfigurations +my_template nox -WebSocket -WebSockets -changelog -async +plotly +postfixed +postprocessing +postprocessor pre prefetch prefetching preloader -whitespace +preprocessor +py +pyodide +pyscript +reactpy refetch refetched refetching -html -jupyter -iframe -keyworded +sanic +serializable stylesheet stylesheets -unstyled -py -reactpy -asgi -postfixed -postprocessing -serializable -postprocessor -preprocessor -middleware -backends -backend -frontend -frontends -misconfiguration -misconfigurations -backhaul sublicense -broadcasted -hello_world -my_template +unstyled +WebSocket +WebSockets +whitespace +pytest +linter +linters +linting +formatters +bootstrap_form 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 dd258737..371893e1 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -8,7 +8,7 @@ If you want to add some interactivity to your existing **Django project**, you d !!! abstract "Note" - These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. + These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/stable/intro/tutorial01/), which involves creating and installing at least one **Django app**. If do not have a **Django project**, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. @@ -24,31 +24,31 @@ pip install reactpy-django ## Step 2: Configure `settings.py` -Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. +Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/stable/topics/settings/) file. === "settings.py" ```python - {% include "../../examples/python/configure-installed-apps.py" %} + {% include "../../examples/python/configure_installed_apps.py" %} ``` ??? warning "Enable ASGI and Django Channels (Required)" ReactPy-Django requires Django ASGI and [Django Channels](https://github.com/django/channels) WebSockets. - If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: + If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: 1. Install `channels[daphne]` 2. Add `#!python "daphne"` to `#!python INSTALLED_APPS`. ```python linenums="0" - {% include "../../examples/python/configure-channels-installed-app.py" %} + {% include "../../examples/python/configure_channels_installed_app.py" %} ``` 3. Set your `#!python ASGI_APPLICATION` variable. ```python linenums="0" - {% include "../../examples/python/configure-channels-asgi-app.py" %} + {% include "../../examples/python/configure_channels_asgi_app.py" %} ``` ??? info "Configure ReactPy settings (Optional)" @@ -59,22 +59,22 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject ## Step 3: Configure `urls.py` -Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. +Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/stable/topics/http/urls/) file. === "urls.py" ```python - {% include "../../examples/python/configure-urls.py" %} + {% include "../../examples/python/configure_urls.py" %} ``` ## Step 4: Configure `asgi.py` -Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. +Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/) file. === "asgi.py" ```python - {% include "../../examples/python/configure-asgi.py" %} + {% include "../../examples/python/configure_asgi.py" %} ``` ??? info "Add `#!python AuthMiddlewareStack` (Optional)" @@ -87,9 +87,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [` In these situations will need to ensure you are using `#!python AuthMiddlewareStack`. - ```python linenums="0" - {% include "../../examples/python/configure-asgi-middleware.py" start="# start" %} - ``` + {% include "../../includes/auth-middleware-stack.md" %} ??? question "Where is my `asgi.py`?" @@ -97,7 +95,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [` ## Step 5: Run database migrations -Run Django's [`migrate` command](https://docs.djangoproject.com/en/dev/topics/migrations/) to initialize ReactPy-Django's database table. +Run Django's [`migrate` command](https://docs.djangoproject.com/en/stable/topics/migrations/) to initialize ReactPy-Django's database table. ```bash linenums="0" python manage.py migrate @@ -105,7 +103,7 @@ python manage.py migrate ## Step 6: Check your configuration -Run Django's [`check` command](https://docs.djangoproject.com/en/dev/ref/django-admin/#check) to verify if ReactPy was set up correctly. +Run Django's [`check` command](https://docs.djangoproject.com/en/stable/ref/django-admin/#check) to verify if ReactPy was set up correctly. ```bash linenums="0" python manage.py check @@ -113,7 +111,7 @@ python manage.py check ## Step 7: Create your first component -The [next step](./your-first-component.md) will show you how to create your first ReactPy component. +The [next page](./your-first-component.md) will show you how to create your first ReactPy component. Prefer a quick summary? Read the **At a Glance** section below. diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md index 08df6a57..e3be5da4 100644 --- a/docs/src/learn/your-first-component.md +++ b/docs/src/learn/your-first-component.md @@ -8,7 +8,7 @@ Components are one of the core concepts of ReactPy. They are the foundation upon !!! abstract "Note" - If you have reached this point, you should have already [installed ReactPy-Django](../learn/add-reactpy-to-a-django-project.md) through the previous steps. + If you have reached this point, you should have already [installed ReactPy-Django](./add-reactpy-to-a-django-project.md) through the previous steps. --- @@ -18,7 +18,7 @@ You will now need to pick at least one **Django app** to start using ReactPy-Dja For the following examples, we will assume the following: -1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). +1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/stable/intro/tutorial01/#creating-the-polls-app). 2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. ??? question "How do I organize my Django project for ReactPy?" @@ -31,7 +31,7 @@ You will need a file to start creating ReactPy components. We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. -Within this file, you can define your component functions using ReactPy's `#!python @component` decorator. +Within this file, you will define your component function(s) using the `#!python @component` decorator. === "components.py" @@ -43,7 +43,7 @@ Within this file, you can define your component functions using ReactPy's `#!pyt We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - Ultimately, components are referenced by Python dotted path in `my_template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. + Ultimately, components are referenced by Python dotted path in `my_template.html` ([_see next step_](#embedding-in-a-template)). This dotted path must be valid to Python's `#!python importlib`. ??? question "What does the decorator actually do?" @@ -66,30 +66,36 @@ Additionally, you can pass in `#!python args` and `#!python kwargs` into your co {% include-markdown "../../../README.md" start="" end="" %} +???+ tip "Components are automatically registered!" + + ReactPy-Django will automatically register any component that is referenced in a Django HTML template. This means you typically do not need to [manually register](../reference/utils.md#register-component) components in your **Django app**. + + Please note that this HTML template must be properly stored within a registered Django app. ReactPy-Django will output a console log message containing all detected components when the server starts up. + {% include-markdown "../reference/template-tag.md" start="" end="" %} {% include-markdown "../reference/template-tag.md" start="" end="" %} ??? question "Where is my templates folder?" - If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). + If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/stable/ref/applications/#configuring-applications). ## Setting up a Django view -Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my_template.html` ([_from the previous step_](#embedding-in-a-template)). +Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/stable/intro/tutorial01/#write-your-first-view) to render the HTML template `my_template.html` ([_from the previous step_](#embedding-in-a-template)). === "views.py" ```python - {% include "../../examples/python/example/views.py" %} + {% include "../../examples/python/first_view.py" %} ``` -We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. +We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/stable/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. === "urls.py" ```python - {% include "../../examples/python/example/urls.py" %} + {% include "../../examples/python/first_urls.py" %} ``` ??? question "Which urls.py do I add my views to?" @@ -98,7 +104,7 @@ We will add this new view into your [`urls.py`](https://docs.djangoproject.com/e Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. - Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. + Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/stable/ref/urls/#include) to link it all together. ## Viewing your component @@ -114,7 +120,7 @@ If you copy-pasted our example component, you will now see your component displa ??? warning "Do not use `manage.py runserver` for production" - This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). + This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/stable/howto/deployment/). ## Learn more diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index aaeabba7..448af463 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -12,24 +12,26 @@ We supply some pre-designed that components can be used to help simplify develop This allows you to embedded any number of client-side PyScript components within traditional ReactPy components. -{% include-markdown "../reference/template-tag.md" start="" end="" %} +{% include-markdown "./template-tag.md" start="" end="" %} + +{% include-markdown "./template-tag.md" start="" end="" %} === "components.py" ```python - {% include "../../examples/python/pyscript-ssr-parent.py" %} + {% include "../../examples/python/pyscript_ssr_parent.py" %} ``` === "root.py" ```python - {% include "../../examples/python/pyscript-ssr-child.py" %} + {% include "../../examples/python/pyscript_ssr_child.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-ssr-parent.html" %} + {% include "../../examples/html/pyscript_ssr_parent.html" %} ``` ??? example "See Interface" @@ -51,31 +53,33 @@ This allows you to embedded any number of client-side PyScript components within === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup.html" %} + {% include "../../examples/html/pyscript_setup.html" %} ``` -{% include-markdown "../reference/template-tag.md" start="" end="" %} +{% include-markdown "./template-tag.md" start="" end="" %} + +{% include-markdown "./template-tag.md" start="" end="" %} -{% include-markdown "../reference/template-tag.md" start="" end="" trailing-newlines=false preserve-includer-indent=false %} +{% include-markdown "./template-tag.md" start="" end="" trailing-newlines=false preserve-includer-indent=false %} === "components.py" ```python - {% include "../../examples/python/pyscript-component-multiple-files-root.py" %} + {% include "../../examples/python/pyscript_component_multiple_files_root.py" %} ``` === "root.py" ```python - {% include "../../examples/python/pyscript-multiple-files-root.py" %} + {% include "../../examples/python/pyscript_multiple_files_root.py" %} ``` === "child.py" ```python - {% include "../../examples/python/pyscript-multiple-files-child.py" %} + {% include "../../examples/python/pyscript_multiple_files_child.py" %} ``` ??? question "How do I display something while the component is loading?" @@ -87,7 +91,7 @@ This allows you to embedded any number of client-side PyScript components within === "components.py" ```python - {% include "../../examples/python/pyscript-component-initial-object.py" %} + {% include "../../examples/python/pyscript_component_initial_object.py" %} ``` However, you can also use a string containing raw HTML. @@ -95,7 +99,7 @@ This allows you to embedded any number of client-side PyScript components within === "components.py" ```python - {% include "../../examples/python/pyscript-component-initial-string.py" %} + {% include "../../examples/python/pyscript_component_initial_string.py" %} ``` ??? question "Can I use a different name for my root component?" @@ -105,13 +109,13 @@ This allows you to embedded any number of client-side PyScript components within === "components.py" ```python - {% include "../../examples/python/pyscript-component-root.py" %} + {% include "../../examples/python/pyscript_component_root.py" %} ``` === "main.py" ```python - {% include "../../examples/python/pyscript-root.py" %} + {% include "../../examples/python/pyscript_root.py" %} ``` --- @@ -122,7 +126,7 @@ Automatically convert a Django view into a component. At this time, this works best with static views with no interactivity. -Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). +Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/stable/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/stable/topics/class-based-views/). === "components.py" @@ -154,11 +158,11 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? info "Existing limitations" - There are currently several limitations of using `#!python view_to_component` that may be resolved in a future version. + There are currently several limitations of using `#!python view_to_component` that will be [resolved in a future version](https://github.com/reactive-python/reactpy-django/issues/269). - Requires manual intervention to change HTTP methods to anything other than `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. - - Has no option to automatically intercept local anchor link (such as `#!html
`) click events. + - Has no option to automatically intercept click events from hyperlinks (such as `#!html `). ??? question "How do I use this for Class Based Views?" @@ -169,7 +173,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-cbv.py" %} + {% include "../../examples/python/vtc_cbv.py" %} ``` === "views.py" @@ -185,7 +189,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-args.py" %} + {% include "../../examples/python/vtc_args.py" %} ``` === "views.py" @@ -213,7 +217,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-strict-parsing.py" %} + {% include "../../examples/python/vtc_strict_parsing.py" %} ``` === "views.py" @@ -235,7 +239,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-transforms.py" %} + {% include "../../examples/python/vtc_transforms.py" %} ``` === "views.py" @@ -252,7 +256,7 @@ Automatically convert a Django view into an [`iframe` element](https://www.techt The contents of this `#!python iframe` is handled entirely by traditional Django view rendering. While this solution is compatible with more views than `#!python view_to_component`, it comes with different limitations. -Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). +Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/stable/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/stable/topics/class-based-views/). === "components.py" @@ -290,12 +294,12 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? info "Existing limitations" - There are currently several limitations of using `#!python view_to_iframe` that may be resolved in a future version. + There are currently several limitations of using `#!python view_to_iframe` which may be [resolved in a future version](https://github.com/reactive-python/reactpy-django/issues/268). - No built-in method of signalling events back to the parent component. - - All provided `#!python *args` and `#!python *kwargs` must be serializable values, since they are encoded into the URL. + - All provided `#!python args` and `#!python kwargs` must be serializable values, since they are encoded into the URL. - The `#!python iframe` will always load **after** the parent component. - - CSS styling for `#!python iframe` elements tends to be awkward/difficult. + - CSS styling for `#!python iframe` elements tends to be awkward. ??? question "How do I use this for Class Based Views?" @@ -306,7 +310,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vti-cbv.py" %} + {% include "../../examples/python/vti_cbv.py" %} ``` === "views.py" @@ -330,7 +334,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vti-args.py" %} + {% include "../../examples/python/vti_args.py" %} ``` === "views.py" @@ -362,7 +366,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vti-extra-props.py" %} + {% include "../../examples/python/vti_extra_props.py" %} ``` === "views.py" @@ -379,14 +383,112 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. --- +## Django Form + +Automatically convert a Django form into a ReactPy component. + +Compatible with both [standard Django forms](https://docs.djangoproject.com/en/stable/topics/forms/#building-a-form) and [ModelForms](https://docs.djangoproject.com/en/stable/topics/forms/modelforms/). + +=== "components.py" + + ```python + {% include "../../examples/python/django_form.py" %} + ``` + +=== "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python form` | `#!python type[Form | ModelForm]` | The form to convert. | N/A | + | `#!python on_success` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form is successfully submitted. | `#!python None` | + | `#!python on_error` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form submission fails. | `#!python None` | + | `#!python on_receive_data` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called before newly submitted form data is rendered. | `#!python None` | + | `#!python on_change` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when a form field is modified by the user. | `#!python None` | + | `#!python auto_save` | `#!python bool` | If `#!python True`, the form will automatically call `#!python save` on successful submission of a `#!python ModelForm`. This has no effect on regular `#!python Form` instances. | `#!python True` | + | `#!python extra_props` | `#!python dict[str, Any] | None` | Additional properties to add to the `#!html
` element. | `#!python None` | + | `#!python extra_transforms` | `#!python Sequence[Callable[[VdomDict], Any]] | None` | A list of functions that transforms the newly generated VDOM. The functions will be repeatedly called on each VDOM node. | `#!python None` | + | `#!python form_template` | `#!python str | None` | The template to use for the form. If `#!python None`, Django's default template is used. | `#!python None` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run event callback functions in thread sensitive mode. This mode only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python top_children` | `#!python Sequence[Any]` | Additional elements to add to the top of the form. | `#!python tuple` | + | `#!python bottom_children` | `#!python Sequence[Any]` | Additional elements to add to the bottom of the form. | `#!python tuple` | + | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings. | `#!python None` | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Component` | A ReactPy component. | + +??? info "Existing limitations" + + The following fields are currently incompatible with `#!python django_form`: `#!python FileField`, `#!python ImageField`, `#!python SplitDateTimeField`, and `#!python MultiValueField`. + + Compatibility for these fields will be [added in a future version](https://github.com/reactive-python/reactpy-django/issues/270). + +??? question "How do I style these forms with Bootstrap?" + + You can style these forms by using a form styling library. In the example below, it is assumed that you have already installed [`django-bootstrap5`](https://pypi.org/project/django-bootstrap5/). + + After installing a form styling library, you can then provide ReactPy a custom `#!python form_template` parameter. This parameter allows you to specify a custom HTML template to use to render this the form. + + Note that you can also set a global default for `form_template` by using [`settings.py:REACTPY_DEFAULT_FORM_TEMPLATE`](./settings.md#reactpy_default_form_template). + + === "components.py" + + ```python + {% include "../../examples/python/django_form_bootstrap.py" %} + ``` + + === "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + + === "bootstrap_form.html" + + ```jinja + {% include "../../examples/html/django_form_bootstrap.html" %} + ``` + +??? question "How do I handle form success/errors?" + + You can react to form state by providing a callback function to any of the following parameters: `#!python on_success`, `#!python on_error`, `#!python on_receive_data`, and `#!python on_change`. + + These functions will be called when the form is submitted. + + In the example below, we will use the `#!python on_success` parameter to change the URL upon successful submission. + + === "components.py" + + ```python + {% include "../../examples/python/django_form_on_success.py" %} + ``` + + === "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + +--- + ## Django CSS -Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). +Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/stable/howto/static-files/). === "components.py" ```python - {% include "../../examples/python/django-css.py" %} + {% include "../../examples/python/django_css.py" %} ``` ??? example "See Interface" @@ -411,7 +513,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. Here's an example on what you should avoid doing for Django static files: ```python - {% include "../../examples/python/django-css-local-link.py" %} + {% include "../../examples/python/django_css_local_link.py" %} ``` ??? question "How do I load external CSS?" @@ -421,7 +523,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. For external CSS, you should use `#!python html.link`. ```python - {% include "../../examples/python/django-css-external-link.py" %} + {% include "../../examples/python/django_css_external_link.py" %} ``` ??? question "Why not load my CSS in `#!html `?" @@ -434,11 +536,11 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. ## Django JS -Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). +Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/stable/howto/static-files/). " end="" %} -??? question "Can I make a failed query try again?" +??? question "Can I force a query to run again?" - Yes, `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` instance. + `#!python use_query` can be re-executed by calling `#!python refetch()` on your `#!python use_query` result. - For example, take a look at `#!python reset_event` below. + The example below uses an `#!python onClick` event to determine when to reset the query. === "components.py" ```python - {% include "../../examples/python/use-mutation-reset.py" %} - ``` - - === "models.py" - - ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/use_query_refetch.py" %} ``` ??? question "Why does the example query function return `#!python TodoItem.objects.all()`?" This design decision was based on [Apollo's `#!javascript useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `#!python SynchronousOnlyOperation` exceptions. - With the `#!python Model` or `#!python QuerySet` your function returns, this hook uses the [default postprocessor](../reference/utils.md#django-query-postprocessor) to ensure that all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields) or [lazy](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) fields are executed. + With the `#!python Model` or `#!python QuerySet` your function returns, this hook uses the [default postprocessor](./utils.md#django-query-postprocessor) to ensure that all [deferred](https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.get_deferred_fields) or [lazy](https://docs.djangoproject.com/en/stable/topics/db/queries/#querysets-are-lazy) fields are executed. --- @@ -172,13 +166,13 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation.py" %} + {% include "../../examples/python/use_mutation.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? example "See Interface" @@ -188,7 +182,7 @@ Mutation functions can be sync or async. | Name | Type | Description | Default | | --- | --- | --- | --- | | `#!python mutation` | `#!python Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | - | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This mode only applies to sync mutation functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A query function (the function you provide to your `#!python use_query` hook) or a sequence of query functions that need a `#!python refetch` if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | `#!python None` | **Returns** @@ -204,7 +198,7 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-args-kwargs.py" %} + {% include "../../examples/python/use_mutation_args_kwargs.py" %} ``` ??? question "How can I customize this hook's behavior?" @@ -217,41 +211,41 @@ Mutation functions can be sync or async. **`#!python thread_sensitive`** - Whether to run your synchronous mutation function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. + Whether to run your synchronous mutation function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/stable/topics/async/#sync-to-async) docs for more information. This setting only applies to sync query functions, and will be ignored for async functions. === "components.py" ```python - {% include "../../examples/python/use-mutation-thread-sensitive.py" %} + {% include "../../examples/python/use_mutation_thread_sensitive.py" %} ``` ??? question "Can I make ORM calls without hooks?" {% include-markdown "../../includes/orm.md" start="" end="" %} -??? question "Can I make a failed mutation try again?" +??? question "Can I force a mutation run again?" - Yes, `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` instance. + `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` result. For example, take a look at `#!python reset_event` below. === "components.py" ```python - {% include "../../examples/python/use-mutation-reset.py" %} + {% include "../../examples/python/use_mutation_reset.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? question "Can `#!python use_mutation` trigger a refetch of `#!python use_query`?" - Yes, `#!python use_mutation` can queue a refetch of a `#!python use_query` via the `#!python refetch=...` argument. + `#!python use_mutation` can queue a refetch of a `#!python use_query` via the `#!python refetch=...` argument. The example below is a merge of the `#!python use_query` and `#!python use_mutation` examples above with the addition of a `#!python use_mutation(refetch=...)` argument. @@ -260,29 +254,106 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-query-refetch.py" %} + {% include "../../examples/python/use_mutation_query_refetch.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` --- +## User Hooks + +--- + +### Use Auth + +Provides a `#!python NamedTuple` containing `#!python async login` and `#!python async logout` functions. + +This hook utilizes the Django's authentication framework in a way that provides **persistent** login. + +=== "components.py" + + ```python + {% include "../../examples/python/use_auth.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python UseAuthTuple` | A named tuple containing `#!python login` and `#!python logout` async functions. | + +??? warning "Extra Django configuration required" + + Your ReactPy WebSocket must utilize `#!python AuthMiddlewareStack` in order to use this hook. + + {% include "../../includes/auth-middleware-stack.md" %} + +??? question "Why use this instead of `#!python channels.auth.login`?" + + The `#!python channels.auth.*` functions cannot trigger re-renders of your ReactPy components. Additionally, they do not provide persistent authentication when used within ReactPy. + + This is a result of Django's authentication design, which requires cookies to retain login status. ReactPy is rendered via WebSockets, and browsers do not allow active WebSocket connections to modify cookies. + + To work around this limitation, when `#!python use_auth().login()` is called within your application, ReactPy performs the following process... + + 1. The server authenticates the user into the WebSocket + 2. The server generates a temporary login token + 3. The server commands the browser to use the login token (via HTTP) + 4. The client performs the HTTP request + 5. The server returns the HTTP response, which contains all necessary cookies + 6. The client stores these cookies in the browser + + This ultimately results in persistent authentication which will be retained even if the browser tab is refreshed. + +--- + +### Use User + +Shortcut that returns the WebSocket or HTTP connection's `#!python User`. + +=== "components.py" + + ```python + {% include "../../examples/python/use_user.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python AbstractUser` | A Django `#!python User`, which can also be an `#!python AnonymousUser`. | + +--- + ### Use User Data -Store or retrieve a `#!python dict` containing user data specific to the connection's `#!python User`. +Store or retrieve a `#!python dict` containing arbitrary data specific to the connection's `#!python User`. -This hook is useful for storing user-specific data, such as preferences, settings, or any generic key-value pairs. +This hook is useful for storing user-specific data, such as preferences or settings. User data saved with this hook is stored within the `#!python REACTPY_DATABASE`. === "components.py" ```python - {% include "../../examples/python/use-user-data.py" %} + {% include "../../examples/python/use_user_data.py" %} ``` ??? example "See Interface" @@ -309,7 +380,7 @@ User data saved with this hook is stored within the `#!python REACTPY_DATABASE`. === "components.py" ```python - {% include "../../examples/python/use-user-data-defaults.py" %} + {% include "../../examples/python/use_user_data_defaults.py" %} ``` --- @@ -320,7 +391,7 @@ User data saved with this hook is stored within the `#!python REACTPY_DATABASE`. ### Use Channel Layer -Subscribe to a [Django Channels layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html) to send/receive messages. +Subscribe to a [Django Channels Layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html) to communicate messages. Layers are a multiprocessing-safe communication system that allows you to send/receive messages between different parts of your application. @@ -329,7 +400,7 @@ This is often used to create chat systems, synchronize data between components, === "components.py" ```python - {% include "../../examples/python/use-channel-layer.py" %} + {% include "../../examples/python/use_channel_layer.py" %} ``` ??? example "See Interface" @@ -338,18 +409,16 @@ This is often used to create chat systems, synchronize data between components, | Name | Type | Description | Default | | --- | --- | --- | --- | - | `#!python name` | `#!python str | None` | The name of the channel to subscribe to. If you define a `#!python group_name`, you can keep `#!python name` undefined to auto-generate a unique name. | `#!python None` | - | `#!python group_name` | `#!python str | None` | If configured, any messages sent within this hook will be broadcasted to all channels in this group. | `#!python None` | - | `#!python group_add` | `#!python bool` | If `#!python True`, the channel will automatically be added to the group when the component mounts. | `#!python True` | - | `#!python group_discard` | `#!python bool` | If `#!python True`, the channel will automatically be removed from the group when the component dismounts. | `#!python True` | - | `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from a channel. If more than one receiver waits on the same channel name, a random receiver will get the result. | `#!python None` | - | `#!python layer` | `#!python str` | The channel layer to use. This layer must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` | + | `#!python channel` | `#!python str | None` | The name of the channel this hook will send/receive messages on. If `#!python group` is defined and `#!python channel` is `#!python None`, ReactPy will automatically generate a unique channel name. | `#!python None` | + | `#!python group` | `#!python str | None` | If configured, the `#!python channel` is added to a `#!python group` and any messages sent by `#!python AsyncMessageSender` is broadcasted to all channels within the `#!python group`. | `#!python None` | + | `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from a channel. | `#!python None` | + | `#!python layer` | `#!python str` | The Django Channels layer to use. This layer must be defined in `settings.py:CHANNEL_LAYERS`. | `#!python 'default'` | **Returns** | Type | Description | | --- | --- | - | `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict`. | + | `#!python AsyncMessageSender` | An async callable that can send messages to the channel(s). This callable accepts a single argument, `#!python message: dict`, which is the data sent to the channel or group of channels. | ??? warning "Extra Django configuration required" @@ -357,11 +426,11 @@ This is often used to create chat systems, synchronize data between components, The [Django Channels documentation](https://channels.readthedocs.io/en/latest/topics/channel_layers.html#configuration) has information on what steps you need to take. - In summary, you will need to: + Here is a short summary of the most common installation steps: 1. Install [`redis`](https://redis.io/download/) on your machine. - 2. Run the following command to install `channels-redis` in your Python environment. + 2. Install `channels-redis` in your Python environment. ```bash linenums="0" pip install channels-redis @@ -380,38 +449,72 @@ This is often used to create chat systems, synchronize data between components, } ``` +??? tip "Learn about the quirks of Django Channel Layers" + + ReactPy tries to simplify the process of using Django Channels Layers, but it is important to understand how they work. + + There are a few quirks of Django Channels Layers to be aware of: + + - Any given `#!python channel` should only have one `#!python receiver` registered to it, under normal circumstances. + - This is why ReactPy automatically generates a unique channel name when using `#!python group`. + - When using `#!python group` within this hook, it is suggested to leave `#!python channel` undefined to let ReactPy automatically create a unique channel name (unless you know what you are doing). + - If you have multiple receivers for the same `#!python channel`, only one receiver will get the result. + - This quirk extends to groups as well. For example, If you have two component instances that use the same `#!python channel` within a `#!python group`, the message will only reach one receiver (for that channel). + - Channels exist independently of their `#!python group`. + - Groups are just a loose collection of channel names where a copy of each message can be sent. + - As a result, Django allows you to send messages directly to a `#!python channel` even if it is within a `#!python group`. + - By default, `#!python RedisChannelLayer` will close groups once they have existed for more than 24 hours. + - You need to create your own subclass of `#!python RedisChannelLayer` to change this behavior. + - By default, `#!python RedisChannelLayer` only allows 100 messages backlogged within a `#!python channel` receive queue. + - Rapidly sending messages can overwhelm this queue, resulting in new messages being dropped. + - If you expect to exceed this limit, you need to create your own subclass of `#!python RedisChannelLayer` to change this behavior. + ??? question "How do I broadcast a message to multiple components?" - If more than one receiver waits on the same channel, a random one will get the result. + Groups allow you to broadcast messages to all channels within that group. If you do not define a `#!python channel` while using groups, ReactPy will automatically generate a unique channel name for you. - To get around this, you can define a `#!python group_name` to broadcast messages to all channels within a specific group. If you do not define a channel `#!python name` while using groups, ReactPy will automatically generate a unique channel name for you. + In the example below, since all components use the same channel `#!python group`, messages sent by `#!python my_sender_component` will reach all existing instances of `#!python my_receiver_component_1` and `#!python my_receiver_component_2`. - In the example below, all messages sent by the `#!python sender` component will be received by all `#!python receiver` components that exist (across every active client browser). + === "components.py" + + ```python + {% include "../../examples/python/use_channel_layer_group.py" %} + ``` + +??? question "How do I send a message to a single component (point-to-point communication)?" + + The most common way of using `#!python use_channel_layer` is to broadcast messages to multiple components via a `#!python group`. + + However, you can also use this hook to establish unidirectional, point-to-point communication towards a single `#!python receiver` function. This is slightly more efficient since it avoids the overhead of `#!python group` broadcasting. + + In the example below, `#!python my_sender_component` will communicate directly to a single instance of `#!python my_receiver_component`. This is achieved by defining a `#!python channel` while omitting the `#!python group` parameter. === "components.py" ```python - {% include "../../examples/python/use-channel-layer-group.py" %} + {% include "../../examples/python/use_channel_layer_single.py" %} ``` + Note that if you have multiple instances of `#!python my_receiver_component` using the same `#!python channel`, only one will receive the message. + ??? question "How do I signal a re-render from something that isn't a component?" - There are occasions where you may want to signal a re-render from something that isn't a component, such as a Django model signal. + There are occasions where you may want to signal to the `#!python use_channel_layer` hook from something that isn't a component, such as a Django [model signal](https://docs.djangoproject.com/en/stable/topics/signals/). In these cases, you can use the `#!python use_channel_layer` hook to receive a signal within your component, and then use the `#!python get_channel_layer().send(...)` to send the signal. - In the example below, the sender will send a signal every time `#!python ExampleModel` is saved. Then, when the receiver component gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render. + In the example below, the sender will signal every time `#!python ExampleModel` is saved. Then, when the receiver gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render. === "signals.py" ```python - {% include "../../examples/python/use-channel-layer-signal-sender.py" %} + {% include "../../examples/python/use_channel_layer_signal_sender.py" %} ``` === "components.py" ```python - {% include "../../examples/python/use-channel-layer-signal-receiver.py" %} + {% include "../../examples/python/use_channel_layer_signal_receiver.py" %} ``` --- @@ -422,12 +525,12 @@ This is often used to create chat systems, synchronize data between components, ### Use Connection -Returns the active connection, which is either a Django [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) or a [HTTP Request](https://docs.djangoproject.com/en/4.2/ref/request-response/#django.http.HttpRequest). +Returns the active connection, which is either a Django [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) or a [HTTP Request](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest). === "components.py" ```python - {% include "../../examples/python/use-connection.py" %} + {% include "../../examples/python/use_connection.py" %} ``` ??? example "See Interface" @@ -451,7 +554,7 @@ Shortcut that returns the WebSocket or HTTP connection's [scope](https://channel === "components.py" ```python - {% include "../../examples/python/use-scope.py" %} + {% include "../../examples/python/use_scope.py" %} ``` ??? example "See Interface" @@ -475,7 +578,7 @@ Shortcut that returns the browser's current `#!python Location`. === "components.py" ```python - {% include "../../examples/python/use-location.py" %} + {% include "../../examples/python/use_location.py" %} ``` ??? example "See Interface" @@ -501,7 +604,7 @@ You can expect this hook to provide strings such as `http://example.com`. === "components.py" ```python - {% include "../../examples/python/use-origin.py" %} + {% include "../../examples/python/use_origin.py" %} ``` ??? example "See Interface" @@ -522,14 +625,14 @@ You can expect this hook to provide strings such as `http://example.com`. Shortcut that returns the root component's `#!python id` from the WebSocket or HTTP connection. -The root ID is currently a randomly generated `#!python uuid4` (unique across all root component). +The root ID is a randomly generated `#!python uuid4`. It is notable to mention that it is persistent across the current connection. The `uuid` is reset only when the page is refreshed. -This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and/or retain a backlog of messages in case that component is disconnected via `#!python use_channel_layer( ... , group_discard=False)`. +This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance. === "components.py" ```python - {% include "../../examples/python/use-root-id.py" %} + {% include "../../examples/python/use_root_id.py" %} ``` ??? example "See Interface" @@ -546,14 +649,14 @@ This is useful when used in combination with [`#!python use_channel_layer`](#use --- -### Use User +### Use Re-render -Shortcut that returns the WebSocket or HTTP connection's `#!python User`. +Returns a function that can be used to trigger a re-render of the entire component tree. === "components.py" ```python - {% include "../../examples/python/use-user.py" %} + {% include "../../examples/python/use_rerender.py" %} ``` ??? example "See Interface" @@ -566,4 +669,4 @@ Shortcut that returns the WebSocket or HTTP connection's `#!python User`. | Type | Description | | --- | --- | - | `#!python AbstractUser` | A Django `#!python User`, which can also be an `#!python AnonymousUser`. | + | `#!python Callable[[], None]` | A function that triggers a re-render of the entire component tree. | diff --git a/docs/src/reference/html.md b/docs/src/reference/html.md index fd63c033..c9bb0108 100644 --- a/docs/src/reference/html.md +++ b/docs/src/reference/html.md @@ -2,7 +2,7 @@

-We supply some pre-generated that HTML nodes can be used to help simplify development. +We supply some HTML elements can be used to help simplify development.

@@ -10,22 +10,22 @@ We supply some pre-generated that HTML nodes can be used to help simplify develo ## PyScript -Primitive HTML tag that is leveraged by [`reactpy_django.components.pyscript_component`](./components.md#pyscript-component). +PyScript code block. The text content of this tag are executed within the PyScript interpreter. This can be used as an alternative to the `#!python reactpy.html.script`. -This can be used as an alternative to the `#!python reactpy.html.script` tag to execute JavaScript and run client-side Python code. +This is a primitive HTML tag that is leveraged by [`reactpy_django.components.pyscript_component`](./components.md#pyscript-component). -Additionally, this tag functions identically to any other tag contained within `#!python reactpy.html`, and can be used in the same way. +The `pyscript` tag functions identically to HTML tags contained within `#!python reactpy.html`. === "components.py" ```python - {% include "../../examples/python/pyscript-tag.py" %} + {% include "../../examples/python/pyscript_tag.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-tag.html" %} + {% include "../../examples/html/pyscript_tag.html" %} ``` -{% include-markdown "../reference/components.md" start="" end="" %} +{% include-markdown "./components.md" start="" end="" %} diff --git a/docs/src/reference/management-commands.md b/docs/src/reference/management-commands.md index 6e09e5a1..fd9ed790 100644 --- a/docs/src/reference/management-commands.md +++ b/docs/src/reference/management-commands.md @@ -10,7 +10,7 @@ ReactPy exposes Django management commands that can be used to perform various R ## Clean ReactPy Command -Command used to manually clean ReactPy data. +Command used to manually clean expired ReactPy data from the database and/or cache. When using this command without arguments, it will perform all cleaning operations. You can limit cleaning to specific operations through arguments such as `--sessions`. diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md index 66ad0f9e..0d23ee99 100644 --- a/docs/src/reference/router.md +++ b/docs/src/reference/router.md @@ -20,15 +20,18 @@ URL router that enables the ability to conditionally render other components bas !!! warning "Pitfall" - All pages where `django_router` is used must have identical, or more permissive URL exposure within Django's [URL patterns](https://docs.djangoproject.com/en/5.0/topics/http/urls/#example). You can think of the router component as a secondary, client-side router. Django still handles the primary server-side routes. + All pages where `#!python django_router` is used must have identical, or more permissive URL exposure within Django's [URL patterns](https://docs.djangoproject.com/en/5.0/topics/http/urls/#example). You can think of the router component as a secondary, client-side router. Django will always handle the initial load of the webpage. - We recommend creating a route with a wildcard `.*` to forward routes to ReactPy. For example... - `#!python re_path(r"^/router/.*$", my_reactpy_view)` + You can duplicate all your URL patterns within both Django and ReactPy, but it's easiest to use a wildcard `.*` within Django's `#!python urlpatterns` and let ReactPy handle the rest. For example... + + ```python linenums="0" + urlpatterns = [ re_path(r"^.*$", my_reactpy_router_view) ] + ``` === "components.py" ```python - {% include "../../examples/python/django-router.py" %} + {% include "../../examples/python/django_router.py" %} ``` ??? example "See Interface" @@ -45,6 +48,6 @@ URL router that enables the ability to conditionally render other components bas | --- | --- | | `#!python VdomDict | None` | The matched component/path after it has been fully rendered. | -??? question "How is this different from `#!python reactpy_router.simple.router`?" +??? question "How is this different from `#!python reactpy_router.browser_router`?" - This component utilizes `reactpy-router` under the hood, but provides a more Django-like URL routing syntax. + The `django_router` component utilizes the same internals as `browser_router`, but provides a more Django-like URL routing syntax. diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index 4751f7ba..50d0b7db 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -6,12 +6,6 @@ These are ReactPy-Django's default settings values. You can modify these values

-!!! abstract "Note" - - The default configuration of ReactPy is suitable for the vast majority of use cases. - - You should only consider changing settings when the necessity arises. - --- ## General Settings @@ -34,7 +28,7 @@ The prefix used for all ReactPy WebSocket and HTTP URLs. **Example Value(s):** `#!python "example_project.postprocessor"`, `#!python None` -Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. +Dotted path to the default postprocessor function used by the [`use_query`](./hooks.md#use-query) hook. Postprocessor functions can be async or sync. Here is an example of a sync postprocessor function: @@ -48,13 +42,29 @@ Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to disable --- +### `#!python REACTPY_DEFAULT_FORM_TEMPLATE` + +**Default:** `#!python None` + +**Example Value(s):** `#!python "my_templates/bootstrap_form.html"` + +File path to the default form template used by the [`django_form`](./components.md#django-form) component. + +This file path must be valid to Django's [template finder](https://docs.djangoproject.com/en/stable/topics/templates/#support-for-template-engines). + +--- + +## Authentication Settings + +--- + ### `#!python REACTPY_AUTH_BACKEND` **Default:** `#!python "django.contrib.auth.backends.ModelBackend"` **Example Value(s):** `#!python "example_project.auth.MyModelBackend"` -Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if: +Dotted path to the Django authentication backend to use for ReactPy components. This is typically needed if: 1. You are using `#!python settings.py:REACTPY_AUTO_RELOGIN=True` and... 2. You are using `#!python AuthMiddlewareStack` and... @@ -63,6 +73,22 @@ Dotted path to the Django authentication backend to use for ReactPy components. --- +### `#!python REACTPY_AUTH_TOKEN_MAX_AGE` + +**Default:** `#!python 30` + +**Example Value(s):** `#!python 5` + +Maximum seconds before ReactPy's login token expires. + +This setting exists because Django's authentication design requires cookies to retain login status. ReactPy is rendered via WebSockets, and browsers do not allow active WebSocket connections to modify cookies. + +To work around this limitation, this setting provides a maximum validity period of a temporary login token. When `#!python reactpy_django.hooks.use_auth().login()` is called within your application, ReactPy will automatically create this temporary login token and command the browser to fetch it via HTTP. + +This setting should be a reasonably low value, but still be high enough to account for a combination of client lag, slow internet, and server response time. + +--- + ### `#!python REACTPY_AUTO_RELOGIN` **Default:** `#!python False` @@ -71,7 +97,7 @@ Dotted path to the Django authentication backend to use for ReactPy components. Enabling this will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. -This is useful to continuously update `#!python last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/dev/topics/http/sessions/). +This is useful to continuously update `#!python last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/stable/topics/http/sessions/). --- @@ -87,7 +113,7 @@ This is useful to continuously update `#!python last_login` timestamps and refre Multiprocessing-safe database used by ReactPy for database-backed hooks and features. -If configuring this value, it is mandatory to enable our database router like such: +If configuring this value, it is mandatory to configure Django to use the ReactPy database router: === "settings.py" @@ -105,7 +131,7 @@ If configuring this value, it is mandatory to enable our database router like su Cache used by ReactPy, typically for caching disk operations. -We recommend using [`redis`](https://docs.djangoproject.com/en/dev/topics/cache/#redis), [`memcache`](https://docs.djangoproject.com/en/dev/topics/cache/#memcached), or [`local-memory caching`](https://docs.djangoproject.com/en/dev/topics/cache/#local-memory-caching). +We recommend using [`redis`](https://docs.djangoproject.com/en/stable/topics/cache/#redis), [`memcache`](https://docs.djangoproject.com/en/stable/topics/cache/#memcached), or [`local-memory caching`](https://docs.djangoproject.com/en/stable/topics/cache/#local-memory-caching). --- @@ -117,12 +143,24 @@ We recommend using [`redis`](https://docs.djangoproject.com/en/dev/topics/cache/ Configures whether ReactPy components are rendered in a dedicated thread. -This allows the web server to process other traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). +This allows the web server to process other traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://github.com/pgjones/hypercorn) and [`uvicorn`](https://www.uvicorn.org/). This setting is incompatible with [`daphne`](https://github.com/django/daphne). --- +### `#!python REACTPY_ASYNC_RENDERING` + +**Default:** `#!python False` + +**Example Value(s):** `#!python True` + +Configures whether to use an async ReactPy rendering queue. When enabled, large renders will no longer block smaller renders from taking place. Additionally, prevents the rendering queue from being blocked on waiting for async effects to startup/shutdown (which is a relatively slow operation). + +This setting is currently in early release, and currently no effort is made to de-duplicate renders. For example, if parent and child components are scheduled to render at the same time, both renders will take place, even though a single render would have been sufficient. + +--- + ### `#!python REACTPY_DEFAULT_HOSTS` **Default:** `#!python None` @@ -133,7 +171,7 @@ 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. This is typically useful for self-hosted applications. -You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) to manually override this default. +You can use the `#!python host` argument in your [template tag](./template-tag.md#component) to manually override this default. --- @@ -152,10 +190,7 @@ During pre-rendering, there are some key differences in behavior: 3. The component will be non-interactive until a WebSocket connection is formed. 4. The component is re-rendered once a WebSocket connection is formed. - - - -You can use the `#!python prerender` argument in your [template tag](../reference/template-tag.md#component) to manually override this default. +You can use the `#!python prerender` argument in your [template tag](./template-tag.md#component) to manually override this default. --- @@ -249,6 +284,16 @@ Configures whether ReactPy should clean up expired component sessions during aut --- +### `#!python REACTPY_CLEAN_AUTH_TOKENS` + +**Default:** `#!python True` + +**Example Value(s):** `#!python False` + +Configures whether ReactPy should clean up expired authentication tokens during automatic clean up operations. + +--- + ### `#!python REACTPY_CLEAN_USER_DATA` **Default:** `#!python True` diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 434c81d0..146dae4c 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -10,7 +10,7 @@ Django template tags can be used within your HTML templates to provide ReactPy f ## Component -This template tag can be used to insert any number of ReactPy components onto your page. +This template tag can be used to insert any number of **server-side** ReactPy components onto your page. Each component loaded via this template tag will receive a dedicated WebSocket connection to the server. @@ -46,20 +46,20 @@ Each component loaded via this template tag will receive a dedicated WebSocket c === "my_template.html" ```jinja - - {% component "example_project.my_app.components.hello_world" recipient="World" %} - {% component my_variable recipient="World" %} + + + {% component "example_project.my_app.components.hello_world" recipient="World" %} ``` === "views.py" ```python - {% include "../../examples/python/template-tag-bad-view.py" %} + {% include "../../examples/python/template_tag_bad_view.py" %} ``` - _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._ + _Note: If you decide to not follow this warning, you will need to use the [`register_component`](./utils.md#register-component) function to manually register your components._ @@ -78,7 +78,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c

{% component "example_project.my_app.components.my_title" %}

{% component "example_project.my_app_2.components.goodbye_world" class="bold small-font" %}

- {% component "example_project.my_app_3.components.simple_button" %} + {% component "example_project.my_app_3.components.my_button" %} ``` @@ -102,12 +102,12 @@ Each component loaded via this template tag will receive a dedicated WebSocket c === "components.py" ```python - {% include "../../examples/python/template-tag-args-kwargs.py" %} + {% include "../../examples/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. + Yes! This is most commonly done through [`settings.py:REACTPY_HOSTS`](./settings.md#reactpy_default_hosts). However, you can use the `#!python host` keyword to render components on a specific ASGI server. === "my_template.html" @@ -127,7 +127,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c ??? 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. + This is most commonly done through [`settings.py:REACTPY_PRERENDER`](./settings.md#reactpy_prerender). However, you can use the `#!python prerender` keyword to pre-render a specific component. === "my_template.html" @@ -157,28 +157,31 @@ This template tag can be used to insert any number of **client-side** ReactPy co -By default, the only dependencies available are the Python standard library, `pyscript`, `pyodide`, `reactpy` core. +By default, the only [available dependencies](./template-tag.md#pyscript-setup) are the Python standard library, `pyscript`, `pyodide`, `reactpy` core. -Your PyScript component file requires a `#!python def root()` component to function as the entry point. +Your Python component file will be directly loaded into the browser. It must have a `#!python def root()` component to act as the entry point. + !!! warning "Pitfall" - Your provided Python file is loaded directly into the client (web browser) **as raw text**, and ran using a PyScript interpreter. Be cautious about what you include in your Python file. + Similar to JavaScript, your provided Python file is loaded directly into the client (web browser) **as raw text** to run using the PyScript interpreter. Be cautious about what you include in your Python file. - As a result of running client-side, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components. + As a result being client-sided, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components. + + === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-component.html" %} + {% include "../../examples/html/pyscript_component.html" %} ``` === "hello_world.py" ```python - {% include "../../examples/python/pyscript-hello-world.py" %} + {% include "../../examples/python/pyscript_hello_world.py" %} ``` ??? example "See Interface" @@ -191,32 +194,65 @@ Your PyScript component file requires a `#!python def root()` component to funct | `#!python initial` | `#!python str | VdomDict | ComponentType` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` | | `#!python root` | `#!python str` | The name of the root component function. | `#!python "root"` | + + +??? tip "Get PyScript type hints within your editor via `webtypy`!" + + By installing the `webtypy` package, you can get type hints for PyScript components within your editor. + + !!! example "Terminal" + + ```bash linenums="0" + pip install webtypy + ``` + + ??? question "How do I execute JavaScript within PyScript components?" - PyScript components have the ability to directly execute standard library JavaScript using the [`pyodide` `js` module](https://pyodide.org/en/stable/usage/type-conversions.html#importing-javascript-objects-into-python) or [`pyscript` foreign function interface](https://docs.pyscript.net/2024.6.1/user-guide/dom/). + PyScript components several options available to execute JavaScript, including... + + - [Pyodide's `js` module](https://pyodide.org/en/stable/usage/type-conversions.html#importing-javascript-objects-into-python) + - [Pyscript's foreign function interface](https://docs.pyscript.net/latest/user-guide/dom/#ffi) + - [Pyscript's JavaScript modules](https://docs.pyscript.net/latest/user-guide/configuration/#javascript-modules). + + **Pyodide JS Module** - The `#!python js` module has access to everything within the browser's JavaScript environment. Therefore, any global JavaScript functions loaded within your HTML `#!html ` can be called as well. However, be mindful of JavaScript load order! + The Pyodide `#!python js` module has access to everything within the browser's JavaScript environment. Therefore, any global JavaScript functions loaded within your HTML `#!html ` can be called as well. However, you will need to be mindful of JavaScript load order if using [`async` or `deferred`](https://javascript.info/script-async-defer) loading! === "root.py" ```python - {% include "../../examples/python/pyscript-js-execution.py" %} + {% include "../../examples/python/pyodide_js_module.py" %} ``` - To import JavaScript modules in a fashion similar to `#!javascript import {moment} from 'static/moment.js'`, you will need to configure your `#!jinja {% pyscript_setup %}` block to make the module available to PyScript. This module will be accessed within `#!python pyscript.js_modules.*`. For more information, see the [PyScript JS modules docs](https://docs.pyscript.net/2024.6.2/user-guide/configuration/#javascript-modules). + **PyScript Foreign Function Interface (FFI)** + + PyScript FFI has similar functionality to Pyodide's `js` module, but utilizes a different API. + + There are two importable modules available that are available within the FFI interface: `window` and `document`. === "root.py" ```python - {% include "../../examples/python/pyscript-js-module.py" %} + {% include "../../examples/python/pyscript_ffi.py" %} + ``` + + **PyScript JS Modules** + + Assuming you have a local bundle stored within your project's static files, you can import JavaScript modules in a fashion similar to `#!javascript import {moment} from 'static/moment.js'`. You will first need to configure your `#!jinja {% pyscript_setup %}` block to make the `moment.js` module available to PyScript. Then, this module can be accessed within `#!python pyscript.js_modules.*`. + + === "root.py" + + ```python + {% include "../../examples/python/pyscript_local_import.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-js-module.html" %} + {% include "../../examples/html/pyscript_local_import.html" %} ``` @@ -238,19 +274,19 @@ Your PyScript component file requires a `#!python def root()` component to funct === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-multiple-files.html" %} + {% include "../../examples/html/pyscript_multiple_files.html" %} ``` === "root.py" ```python - {% include "../../examples/python/pyscript-multiple-files-root.py" %} + {% include "../../examples/python/pyscript_multiple_files_root.py" %} ``` === "child.py" ```python - {% include "../../examples/python/pyscript-multiple-files-child.py" %} + {% include "../../examples/python/pyscript_multiple_files_child.py" %} ``` ??? question "How do I display something while the component is loading?" @@ -262,7 +298,7 @@ Your PyScript component file requires a `#!python def root()` component to funct === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-initial-string.html" %} + {% include "../../examples/html/pyscript_initial_string.html" %} ``` However, you can also insert a `#!python reactpy.html` snippet or a non-interactive `#!python @component` via template context. @@ -270,13 +306,13 @@ Your PyScript component file requires a `#!python def root()` component to funct === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-initial-object.html" %} + {% include "../../examples/html/pyscript_initial_object.html" %} ``` === "views.py" ```python - {% include "../../examples/python/pyscript-initial-object.py" %} + {% include "../../examples/python/pyscript_initial_object.py" %} ``` ??? question "Can I use a different name for my root component?" @@ -286,25 +322,25 @@ Your PyScript component file requires a `#!python def root()` component to funct === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-root.html" %} + {% include "../../examples/html/pyscript_root.html" %} ``` === "main.py" ```python - {% include "../../examples/python/pyscript-root.py" %} + {% include "../../examples/python/pyscript_root.py" %} ``` ## PyScript Setup This template tag configures the current page to be able to run `pyscript`. -You can optionally use this tag to configure the current PyScript environment. For example, you can include a list of Python packages to automatically install within the PyScript environment. +You can optionally use this tag to configure the current PyScript environment, such as adding dependencies. === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup.html" %} + {% include "../../examples/html/pyscript_setup.html" %} ``` ??? example "See Interface" @@ -326,7 +362,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-dependencies.html" %} + {% include "../../examples/html/pyscript_setup_dependencies.html" %} ``` ??? question "How do I install additional Javascript dependencies?" @@ -336,13 +372,13 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-extra-js-object.html" %} + {% include "../../examples/html/pyscript_setup_extra_js_object.html" %} ``` === "views.py" ```python - {% include "../../examples/python/pyscript-setup-extra-js-object.py" %} + {% include "../../examples/python/pyscript_setup_extra_js_object.py" %} ``` The value for `#!python extra_js` is most commonly a Python dictionary, but JSON strings are also supported. @@ -350,7 +386,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-extra-js-string.html" %} + {% include "../../examples/html/pyscript_setup_extra_js_string.html" %} ``` ??? question "How do I modify the `pyscript` default configuration?" @@ -360,7 +396,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-config-string.html" %} + {% include "../../examples/html/pyscript_setup_config_string.html" %} ``` While this value is most commonly a JSON string, Python dictionary objects are also supported. @@ -368,11 +404,25 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-config-object.html" %} + {% include "../../examples/html/pyscript_setup_config_object.html" %} ``` === "views.py" ```python - {% include "../../examples/python/pyscript-setup-config-object.py" %} + {% include "../../examples/python/pyscript_setup_config_object.py" %} + ``` + +??? question "Can I use a local interpreter for PyScript?" + + Yes, you can set up a local interpreter by following PyScript's [standard documentation](https://docs.pyscript.net/latest/user-guide/offline/#local-pyodide-packages). + + To summarize, + + 1. Download the latest Pyodide bundle from the [Pyodide GitHub releases page](https://github.com/pyodide/pyodide/releases) (for example `pyodide-0.26.3.tar.bz2`). + 2. Extract the contents of the bundle to your project's static files. + 3. Configure your `#!jinja {% pyscript_setup %}` template tag to use `pyodide` as an interpreter. + + ```jinja linenums="0" + {% include "../../examples/html/pyscript_setup_local_interpreter.html" %} ``` diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index 6590012c..c5887d04 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -14,9 +14,9 @@ Utility functions provide various miscellaneous functionality for advanced use c ## Register Iframe -This function is used register a view as an `#!python iframe` with ReactPy. +This function is used register a Django view as a ReactPy `#!python iframe`. -It is mandatory to use this function alongside [`view_to_iframe`](../reference/components.md#view-to-iframe). +It is mandatory to use this function alongside [`view_to_iframe`](./components.md#view-to-iframe). === "apps.py" @@ -36,9 +36,9 @@ It is mandatory to use this function alongside [`view_to_iframe`](../reference/c `#!python None` -??? warning "Only use this within `#!python MyAppConfig.ready()`" +??? warning "Only use this within `#!python AppConfig.ready()`" - You should always call `#!python register_iframe` within a Django [`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. + You should always call `#!python register_iframe` within a Django [`AppConfig.ready()` method](https://docs.djangoproject.com/en/stable/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. --- @@ -51,7 +51,7 @@ Typically, this function is automatically called on all components contained wit === "apps.py" ```python - {% include "../../examples/python/register-component.py" %} + {% include "../../examples/python/register_component.py" %} ``` ??? example "See Interface" @@ -68,7 +68,7 @@ Typically, this function is automatically called on all components contained wit ??? warning "Only use this within `#!python MyAppConfig.ready()`" - You should always call `#!python register_component` within a Django [`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. + You should always call `#!python register_component` within a Django [`MyAppConfig.ready()` method](https://docs.djangoproject.com/en/stable/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?" @@ -76,7 +76,7 @@ Typically, this function is automatically called on all components contained wit For security reasons, ReactPy requires all root components to be registered. However, all components contained within Django templates are automatically registered. - This function is commonly needed when you have configured your [`host`](../reference/template-tag.md#component) to a dedicated Django rendering application that doesn't have templates. + This function is commonly needed when you have configured your [`host`](./template-tag.md#component) to a dedicated Django rendering application that doesn't have templates. --- @@ -84,18 +84,18 @@ Typically, this function is automatically called on all components contained wit This is the default postprocessor for the `#!python use_query` hook. -Since ReactPy is rendered within an `#!python asyncio` loop, this postprocessor is exists to prevent Django's `#!python SynchronousOnlyException` by recursively prefetching fields within a `#!python Model` or `#!python QuerySet`. This prefetching step works to eliminate Django's [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) behavior. +Since ReactPy is rendered within an `#!python asyncio` loop, this postprocessor is exists to prevent Django's `#!python SynchronousOnlyException` by recursively prefetching fields within Django's ORM. Note that this effectively eliminates Django's [lazy execution](https://docs.djangoproject.com/en/stable/topics/db/queries/#querysets-are-lazy) behavior. === "components.py" ```python - {% include "../../examples/python/django-query-postprocessor.py" %} + {% include "../../examples/python/django_query_postprocessor.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? example "See Interface" diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 7c228143..00000000 --- a/noxfile.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -from glob import glob -from pathlib import Path - -from nox import Session, session - -ROOT_DIR = Path(__file__).parent - - -@session(tags=["test"]) -def test_python(session: Session) -> None: - """Run the Python-based test suite""" - install_requirements_file(session, "test-env") - session.install(".[all]") - session.chdir(ROOT_DIR / "tests") - session.env["REACTPY_DEBUG_MODE"] = "1" - - posargs = session.posargs[:] - if "--headless" in posargs: - posargs.remove("--headless") - session.env["PLAYWRIGHT_HEADLESS"] = "1" - - if "--no-debug-mode" not in posargs: - posargs.append("--debug-mode") - - session.run("playwright", "install", "chromium") - - # Run tests for each settings file (tests/test_app/settings_*.py) - settings_glob = "test_app/settings_*.py" - settings_files = glob(settings_glob) - assert settings_files, f"No Django settings files found at '{settings_glob}'!" - for settings_file in settings_files: - settings_module = settings_file.strip(".py").replace("/", ".").replace("\\", ".") - session.run( - "python", - "manage.py", - "test", - *posargs, - "-v", - "2", - "--settings", - settings_module, - ) - - -@session(tags=["test"]) -def test_types(session: Session) -> None: - install_requirements_file(session, "check-types") - install_requirements_file(session, "pkg-deps") - session.run("mypy", "--show-error-codes", "src/reactpy_django", "tests/test_app") - - -@session(tags=["test"]) -def test_style(session: Session) -> None: - """Check that style guidelines are being followed""" - install_requirements_file(session, "check-style") - session.run("ruff", "check", ".") - - -@session(tags=["test"]) -def test_javascript(session: Session) -> None: - install_requirements_file(session, "test-env") - session.chdir(ROOT_DIR / "src" / "js") - session.run("python", "-m", "nodejs.npm", "install", external=True) - session.run("python", "-m", "nodejs.npm", "run", "check") - - -def install_requirements_file(session: Session, name: str) -> None: - session.install("--upgrade", "pip", "setuptools", "wheel") - file_path = ROOT_DIR / "requirements" / f"{name}.txt" - assert file_path.exists(), f"requirements file {file_path} does not exist" - session.install("-r", str(file_path)) diff --git a/pyproject.toml b/pyproject.toml index 274a352e..64d530e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,251 @@ [build-system] -requires = ["setuptools>=42", "wheel", "nodejs-bin==18.4.0a4"] -build-backend = "setuptools.build_meta" +build-backend = "hatchling.build" +requires = ["hatchling", "hatch-build-scripts"] -[tool.mypy] -exclude = ['migrations/.*'] -ignore_missing_imports = true -warn_unused_configs = true -warn_redundant_casts = true -warn_unused_ignores = true -check_untyped_defs = true +############################## +# >>> Hatch Build Config <<< # +############################## -[tool.ruff.lint.isort] -known-first-party = ["src", "tests"] +[project] +name = "reactpy_django" +description = "It's React, but in Python. Now with Django integration." +readme = "README.md" +keywords = [ + "React", + "ReactJS", + "ReactPy", + "components", + "asgi", + "django", + "http", + "server", + "reactive", + "interactive", +] +license = "MIT" +authors = [{ name = "Mark Bakhit", email = "archiethemonger@gmail.com" }] +requires-python = ">=3.9" +classifiers = [ + "Framework :: Django", + "Framework :: Django :: 4.0", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Widget Sets", + "Topic :: Software Development :: User Interfaces", + "Environment :: Web Environment", + "Typing :: Typed", +] +dependencies = [ + "channels>=4.0.0", + "django>=4.2.0", + "reactpy>=1.1.0, <2.0.0", + "reactpy-router>=1.0.3, <2.0.0", + "dill>=0.3.5", + "orjson>=3.6.0", + "nest_asyncio>=1.5.0", + "typing_extensions", +] +dynamic = ["version"] +urls.Changelog = "https://reactive-python.github.io/reactpy-django/latest/about/changelog/" +urls.Documentation = "https://reactive-python.github.io/reactpy-django/" +urls.Source = "https://github.com/reactive-python/reactpy-django" -[tool.ruff.lint] -ignore = ["E501"] +[tool.hatch.version] +path = "src/reactpy_django/__init__.py" + +[tool.hatch.build.targets.sdist] +include = ["/src"] +artifacts = ["/src/reactpy_django/static/"] + +[tool.hatch.build.targets.wheel] +artifacts = ["/src/reactpy_django/static/"] + +[tool.hatch.metadata] +license-files = { paths = ["LICENSE.md"] } + +[tool.hatch.envs.default] +installer = "uv" + +[[tool.hatch.build.hooks.build-scripts.scripts]] +commands = [ + "bun install --cwd src/js", + 'bun build src/js/src/index.ts --outdir="src/reactpy_django/static/reactpy_django/" --minify --sourcemap=linked', + 'cd src/build_scripts && python copy_dir.py "src/js/node_modules/@pyscript/core/dist" "src/reactpy_django/static/reactpy_django/pyscript"', + 'cd src/build_scripts && python copy_dir.py "src/js/node_modules/morphdom/dist" "src/reactpy_django/static/reactpy_django/morphdom"', +] +artifacts = [] + +############################# +# >>> Hatch Test Runner <<< # +############################# + +[tool.hatch.envs.hatch-test] +extra-dependencies = [ + "pytest-sugar", + "pytest-django", + "playwright", + "channels[daphne]>=4.0.0", + "twisted", + "tblib", + "servestatic", + "django-bootstrap5", + "decorator", + +] +matrix-name-format = "{variable}-{value}" + +# Django 4.2 +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.9", "3.10", "3.11", "3.12"] +django = ["4.2"] + +# Django 5.0 +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.10", "3.11", "3.12"] +django = ["5.0"] + +# Django 5.1 +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.10", "3.11", "3.12", "3.13"] +django = ["5.1"] + +[tool.hatch.envs.hatch-test.overrides] +matrix.django.dependencies = [ + { if = [ + "4.2", + ], value = "django~=4.2" }, + { if = [ + "5.0", + ], value = "django~=5.0" }, + { if = [ + "5.1", + ], value = "django~=5.1" }, +] + +[tool.pytest.ini_options] +addopts = """\ + --strict-config + --strict-markers + --reuse-db + --maxfail=10 + """ +django_find_project = false +DJANGO_SETTINGS_MODULE = "test_app.settings_single_db" +pythonpath = [".", "tests/"] + +################################ +# >>> Hatch Django Scripts <<< # +################################ + +[tool.hatch.envs.django] +extra-dependencies = [ + "channels[daphne]>=4.0.0", + "twisted", + "servestatic", + "django-bootstrap5", + "decorator", + "playwright", +] + +[tool.hatch.envs.django.scripts] +runserver = [ + "cd tests && python manage.py migrate --noinput", + "cd tests && python manage.py runserver", +] +makemigrations = ["cd tests && python manage.py makemigrations"] +clean = ["cd tests && python manage.py clean_reactpy -v 3"] +clean_sessions = ["cd tests && python manage.py clean_reactpy --sessions -v 3"] +clean_auth_tokens = [ + "cd tests && python manage.py clean_reactpy --auth-tokens -v 3", +] +clean_user_data = [ + "cd tests && python manage.py clean_reactpy --user-data -v 3", +] + +####################################### +# >>> Hatch Documentation Scripts <<< # +####################################### + +[tool.hatch.envs.docs] +template = "docs" +extra-dependencies = [ + "mkdocs", + "mkdocs-git-revision-date-localized-plugin", + "mkdocs-material==9.4.0", + "mkdocs-include-markdown-plugin", + "mkdocs-spellcheck[all]", + "mkdocs-git-authors-plugin", + "mkdocs-minify-plugin", + "mike", + "ruff", + "django-stubs", + "linkcheckmd", +] + +[tool.hatch.envs.docs.scripts] +serve = ["cd docs && mkdocs serve"] +build = ["cd docs && mkdocs build --strict"] +linkcheck = [ + "linkcheckMarkdown docs/ -v -r --method head", + "linkcheckMarkdown README.md -v -r", + "linkcheckMarkdown CHANGELOG.md -v -r", +] +deploy_latest = ["cd docs && mike deploy --push --update-aliases {args} latest"] +deploy_develop = ["cd docs && mike deploy --push develop"] + +################################ +# >>> Hatch Python Scripts <<< # +################################ + +[tool.hatch.envs.python] +extra-dependencies = ["django-stubs", "channels-redis", "pyright"] + +[tool.hatch.envs.python.scripts] +type_check = ["pyright src"] + +############################ +# >>> Hatch JS Scripts <<< # +############################ + +[tool.hatch.envs.javascript] +detached = true + +[tool.hatch.envs.javascript.scripts] +check = ["cd src/js && bun install", "cd src/js && bun run check"] +fix = ["cd src/js && bun install", "cd src/js && bun run format"] + +######################### +# >>> Generic Tools <<< # +######################### [tool.ruff] -extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", ".nox/*", "build/*"] +extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", "build/*"] line-length = 120 +format.preview = true +lint.extend-ignore = [ + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "ARG004", # Unused static method argument + "FBT001", # Boolean-typed positional argument in function definition + "FBT002", # Boolean default positional argument in function definition + "PLR2004", # Magic value used in comparison + "SIM115", # Use context handler for opening files + "SLF001", # Private member accessed + "E501", # Line too long + "PLC0415", # `import` should be at the top-level of a file + "BLE001", # Do not catch blind exception: `Exception` + "PLW0603", # Using global statement is discouraged + "PLR6301", # Method could be a function, class method, or static method + "S403", # `dill` module is possibly insecure + "S301", # `dill` deserialization is possibly insecure unless using trusted data + "RUF029", # Function is declared async but doesn't contain await expression +] +lint.preview = true +lint.isort.known-first-party = ["reactpy_django", "test_app", "example"] +lint.isort.known-third-party = ["js"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 63e3d68e..00000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ --r requirements/build-docs.txt --r requirements/build-pkg.txt --r requirements/check-style.txt --r requirements/check-types.txt --r requirements/dev-env.txt --r requirements/pkg-deps.txt --r requirements/test-env.txt --r requirements/test-run.txt diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt deleted file mode 100644 index f32563ca..00000000 --- a/requirements/build-docs.txt +++ /dev/null @@ -1,10 +0,0 @@ -mkdocs -mkdocs-git-revision-date-localized-plugin -mkdocs-material==9.4.0 -mkdocs-include-markdown-plugin -linkcheckmd -mkdocs-spellcheck[all] -mkdocs-git-authors-plugin -mkdocs-minify-plugin -mkdocs-section-index -mike diff --git a/requirements/build-pkg.txt b/requirements/build-pkg.txt deleted file mode 100644 index 82f40eaf..00000000 --- a/requirements/build-pkg.txt +++ /dev/null @@ -1,3 +0,0 @@ -twine -wheel -build diff --git a/requirements/check-style.txt b/requirements/check-style.txt deleted file mode 100644 index af3ee576..00000000 --- a/requirements/check-style.txt +++ /dev/null @@ -1 +0,0 @@ -ruff diff --git a/requirements/check-types.txt b/requirements/check-types.txt deleted file mode 100644 index c962b716..00000000 --- a/requirements/check-types.txt +++ /dev/null @@ -1,3 +0,0 @@ -mypy -django-stubs[compatible-mypy] -channels-redis diff --git a/requirements/dev-env.txt b/requirements/dev-env.txt deleted file mode 100644 index 05940702..00000000 --- a/requirements/dev-env.txt +++ /dev/null @@ -1,3 +0,0 @@ -twine -wheel --r ./test-run.txt diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt deleted file mode 100644 index c6102c18..00000000 --- a/requirements/pkg-deps.txt +++ /dev/null @@ -1,9 +0,0 @@ -channels >=4.0.0 -django >=4.2.0 -reactpy >=1.0.2, <1.1.0 -reactpy-router >=0.1.1, <1.0.0 -aiofile >=3.0 -dill >=0.3.5 -orjson >=3.6.0 -nest_asyncio >=1.5.0 -typing_extensions diff --git a/requirements/test-env.txt b/requirements/test-env.txt deleted file mode 100644 index 6f146715..00000000 --- a/requirements/test-env.txt +++ /dev/null @@ -1,6 +0,0 @@ -playwright -twisted -channels[daphne]>=4.0.0 -tblib -whitenoise -nodejs-bin==18.4.0a4 diff --git a/requirements/test-run.txt b/requirements/test-run.txt deleted file mode 100644 index 816817c6..00000000 --- a/requirements/test-run.txt +++ /dev/null @@ -1 +0,0 @@ -nox diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 76a91edf..00000000 --- a/setup.py +++ /dev/null @@ -1,159 +0,0 @@ -from __future__ import annotations, print_function - -import shutil -import sys -import traceback -from distutils import log -from pathlib import Path - -from nodejs import npm -from setuptools import find_namespace_packages, setup -from setuptools.command.develop import develop -from setuptools.command.sdist import sdist - -# ----------------------------------------------------------------------------- -# Basic Constants -# ----------------------------------------------------------------------------- -name = "reactpy_django" -root_dir = Path(__file__).parent -src_dir = root_dir / "src" -js_dir = src_dir / "js" -package_dir = src_dir / name -static_dir = package_dir / "static" / name - - -# ----------------------------------------------------------------------------- -# Package Definition -# ----------------------------------------------------------------------------- -package = { - "name": name, - "python_requires": ">=3.9", - "packages": find_namespace_packages(src_dir), - "package_dir": {"": "src"}, - "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", - "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", - "Programming Language :: Python :: 3.12", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Topic :: Multimedia :: Graphics", - "Environment :: Web Environment", - ], -} - - -# ----------------------------------------------------------------------------- -# Library Version -# ----------------------------------------------------------------------------- -for line in (package_dir / "__init__.py").read_text().split("\n"): - if line.startswith("__version__ = "): - package["version"] = eval(line.split("=", 1)[1]) - break -else: - print(f"No version found in {package_dir}/__init__.py") - sys.exit(1) - - -# ----------------------------------------------------------------------------- -# Requirements -# ----------------------------------------------------------------------------- -requirements: list[str] = [] -with (root_dir / "requirements" / "pkg-deps.txt").open() as f: - requirements.extend(line for line in map(str.strip, f) if not line.startswith("#")) -package["install_requires"] = requirements - - -# ----------------------------------------------------------------------------- -# Library Description -# ----------------------------------------------------------------------------- -with (root_dir / "README.md").open() as f: - long_description = f.read() - -package["long_description"] = long_description -package["long_description_content_type"] = "text/markdown" - - -# ---------------------------------------------------------------------------- -# Build Javascript -# ---------------------------------------------------------------------------- -def build_javascript_first(build_cls: type): - class Command(build_cls): - def run(self): - - log.info("Installing Javascript...") - result = npm.call(["install"], cwd=str(js_dir)) - if result != 0: - log.error(traceback.format_exc()) - log.error("Failed to install Javascript") - raise RuntimeError("Failed to install Javascript") - - log.info("Building Javascript...") - result = npm.call(["run", "build"], cwd=str(js_dir)) - if result != 0: - log.error(traceback.format_exc()) - log.error("Failed to build Javascript") - raise RuntimeError("Failed to build Javascript") - - log.info("Copying @pyscript/core distribution") - pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist" - pyscript_static_dir = static_dir / "pyscript" - if not pyscript_static_dir.exists(): - pyscript_static_dir.mkdir() - for file in pyscript_dist.iterdir(): - shutil.copy(file, pyscript_static_dir / file.name) - - log.info("Copying Morphdom distribution") - morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist" - morphdom_static_dir = static_dir / "morphdom" - if not morphdom_static_dir.exists(): - morphdom_static_dir.mkdir() - for file in morphdom_dist.iterdir(): - shutil.copy(file, morphdom_static_dir / file.name) - - log.info("Successfully built Javascript") - super().run() - - return Command - - -package["cmdclass"] = { - "sdist": build_javascript_first(sdist), - "develop": build_javascript_first(develop), -} - -if sys.version_info < (3, 10, 6): - from distutils.command.build import build - - package["cmdclass"]["build"] = build_javascript_first(build) -else: - from setuptools.command.build_py import build_py - - package["cmdclass"]["build_py"] = build_javascript_first(build_py) - - -# ----------------------------------------------------------------------------- -# Installation -# ----------------------------------------------------------------------------- -if __name__ == "__main__": - setup(**package) diff --git a/src/build_scripts/copy_dir.py b/src/build_scripts/copy_dir.py new file mode 100644 index 00000000..0a2cafab --- /dev/null +++ b/src/build_scripts/copy_dir.py @@ -0,0 +1,33 @@ +# ruff: noqa: INP001 +import logging +import shutil +import sys +from pathlib import Path + + +def copy_files(source: Path, destination: Path) -> None: + if destination.exists(): + shutil.rmtree(destination) + destination.mkdir() + + for file in source.iterdir(): + if file.is_file(): + shutil.copy(file, destination / file.name) + else: + copy_files(file, destination / file.name) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + logging.error("Script used incorrectly!\nUsage: python copy_dir.py ") + sys.exit(1) + + root_dir = Path(__file__).parent.parent.parent + src = Path(root_dir / sys.argv[1]) + dest = Path(root_dir / sys.argv[2]) + + if not src.exists(): + logging.error("Source directory %s does not exist", src) + sys.exit(1) + + copy_files(src, dest) diff --git a/src/js/bun.lockb b/src/js/bun.lockb new file mode 100644 index 00000000..0bb863ce Binary files /dev/null and b/src/js/bun.lockb differ diff --git a/src/js/eslint.config.mjs b/src/js/eslint.config.mjs new file mode 100644 index 00000000..320e9f8b --- /dev/null +++ b/src/js/eslint.config.mjs @@ -0,0 +1,43 @@ +import react from "eslint-plugin-react"; +import globals from "globals"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + ...compat.extends("eslint:recommended", "plugin:react/recommended"), + { + plugins: { + react, + }, + + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + + ecmaVersion: "latest", + sourceType: "module", + }, + + settings: { + react: { + version: "18.2.0", + }, + }, + + rules: { + "react/prop-types": "off", + }, + }, +]; diff --git a/src/js/package-lock.json b/src/js/package-lock.json deleted file mode 100644 index d4cb1c0b..00000000 --- a/src/js/package-lock.json +++ /dev/null @@ -1,3337 +0,0 @@ -{ - "name": "js", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@pyscript/core": "^0.4.48", - "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^11.1.6", - "morphdom": "^2.7.3", - "tslib": "^2.6.2" - }, - "devDependencies": { - "@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", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "prettier": "^3.2.3", - "rollup": "^4.9.5", - "typescript": "^5.3.3" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "node_modules/@jridgewell/sourcemap-codec": { - "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/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pyscript/core": { - "version": "0.4.48", - "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.48.tgz", - "integrity": "sha512-cVZ//1WDkWhjZ1tOjUB1YJ5mKxDf3kMpzS/pw7Oe9/BMrB/NM3TxxCQ9Oyvq7Fkfv1F+srIcsi1xZ5gQeP+5Tg==", - "dependencies": { - "@ungap/with-resolvers": "^0.1.0", - "basic-devtools": "^0.1.6", - "polyscript": "^0.13.5", - "sticky-module": "^0.1.1", - "to-json-callback": "^0.1.1", - "type-checked-collections": "^0.1.7" - } - }, - "node_modules/@reactpy/client": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz", - "integrity": "sha512-mvFwAvmRMgo7lTjkhkEJzBep6HX/wfm5BaNbtEMOUzto7G/h+z1AmqlOMXLH37DSI0iwfmCuNwy07EJM0JWZ0g==", - "dependencies": { - "event-to-object": "^0.1.2", - "json-pointer": "^0.6.2" - }, - "peerDependencies": { - "react": ">=16 <18", - "react-dom": ">=16 <18" - } - }, - "node_modules/@rollup/plugin-commonjs": { - "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", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "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.1", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "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.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-typescript": { - "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.1.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.14.0||^3.0.0||^4.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "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", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "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.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.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": "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": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "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": "*" - } - }, - "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.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/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@ungap/with-resolvers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", - "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" - }, - "node_modules/@webreflection/fetch": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", - "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" - }, - "node_modules/@webreflection/idb-map": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.1.tgz", - "integrity": "sha512-lRCanqwR7tHHFohJHAMSMEZnoNPvgjcKr0f5e4y+lTJA+fctT61EZ+f5pT5/+8+wlSsMAvXjzfKRLT6o9aqxbA==" - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", - "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/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 - }, - "node_modules/basic-devtools": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", - "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" - }, - "node_modules/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/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, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/codedent": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", - "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", - "dependencies": { - "plain-tag": "^0.1.3" - } - }, - "node_modules/coincident": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", - "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", - "dependencies": { - "@ungap/structured-clone": "^1.2.0", - "@ungap/with-resolvers": "^0.1.0", - "gc-hook": "^0.3.1", - "proxy-target": "^3.0.2" - }, - "optionalDependencies": { - "ws": "^8.16.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "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" - } - }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", - "dev": true, - "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/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==" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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==", - "dependencies": { - "json-pointer": "^0.6.2" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" - }, - "node_modules/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 - }, - "node_modules/fsevents": { - "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, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "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/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gc-hook": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", - "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/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, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-builtin-module": { - "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" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "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": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/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 - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/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, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", - "dependencies": { - "foreach": "^2.0.4" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/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==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/magic-string": { - "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.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/morphdom": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.3.tgz", - "integrity": "sha512-rvGK92GxSuPEZLY8D/JH07cG3BxyA+/F0Bxg32OoGAEFFhGWA3OqVpqPZlOgZTCR52clXrmz+z2pYSJ6gOig1w==" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", - "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/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, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/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==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plain-tag": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", - "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" - }, - "node_modules/polyscript": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.13.5.tgz", - "integrity": "sha512-PwXWnhLbOMtvZWFIN271JhaN7KnxESaMtv9Rcdrq1TKTCMnkz9idvYb3Od1iumBJlr49lLlwyUKeGb423rFR4w==", - "dependencies": { - "@ungap/structured-clone": "^1.2.0", - "@ungap/with-resolvers": "^0.1.0", - "@webreflection/fetch": "^0.1.5", - "@webreflection/idb-map": "^0.3.1", - "basic-devtools": "^0.1.6", - "codedent": "^0.1.2", - "coincident": "^1.2.3", - "gc-hook": "^0.3.1", - "html-escaper": "^3.0.3", - "proxy-target": "^3.0.2", - "sticky-module": "^0.1.1", - "to-json-callback": "^0.1.1" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "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" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/proxy-target": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", - "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/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, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "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.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/rollup": { - "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": ">=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" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.1", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sticky-module": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", - "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", - "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/to-json-callback": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", - "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-checked-collections": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", - "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "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": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/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 - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "optional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/src/js/package.json b/src/js/package.json index 949b6cf9..bfcdb72c 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,29 +1,20 @@ { - "description": "ReactPy-Django Client", - "main": "src/index.tsx", "type": "module", "scripts": { - "build": "rollup --config", "format": "prettier --write . && eslint --fix", "check": "prettier --check . && eslint" }, "devDependencies": { - "@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", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "prettier": "^3.2.3", - "rollup": "^4.9.5", - "typescript": "^5.3.3" + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.1", + "prettier": "^3.3.3" }, "dependencies": { - "@pyscript/core": "^0.4.48", - "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^11.1.6", - "morphdom": "^2.7.3", - "tslib": "^2.6.2" + "@pyscript/core": "^0.6", + "@reactpy/client": "^0.3.2", + "event-to-object": "^0.1.2", + "morphdom": "^2.7.4" } } diff --git a/src/js/rollup.config.mjs b/src/js/rollup.config.mjs deleted file mode 100644 index 5a6de090..00000000 --- a/src/js/rollup.config.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import resolve from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; -import replace from "@rollup/plugin-replace"; -import typescript from "@rollup/plugin-typescript"; - -export default { - input: "src/index.tsx", - output: { - file: "../reactpy_django/static/reactpy_django/client.js", - format: "esm", - }, - plugins: [ - resolve(), - commonjs(), - replace({ - "process.env.NODE_ENV": JSON.stringify("production"), - }), - typescript(), - ], - onwarn: function (warning) { - console.warn(warning.message); - }, -}; diff --git a/src/js/src/client.ts b/src/js/src/client.ts index 93d2f00b..1f506f56 100644 --- a/src/js/src/client.ts +++ b/src/js/src/client.ts @@ -19,33 +19,33 @@ export class ReactPyDjangoClient constructor(props: ReactPyDjangoClientProps) { super(); this.urls = props.urls; + this.mountElement = props.mountElement; + this.prerenderElement = props.prerenderElement; + this.offlineElement = props.offlineElement; this.socket = createReconnectingWebSocket({ - readyPromise: this.ready, url: this.urls.componentUrl, - onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)), + readyPromise: this.ready, ...props.reconnectOptions, + onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)), onClose: () => { // If offlineElement exists, show it and hide the mountElement/prerenderElement if (this.prerenderElement) { this.prerenderElement.remove(); this.prerenderElement = null; } - if (this.offlineElement) { + if (this.offlineElement && this.mountElement) { this.mountElement.hidden = true; this.offlineElement.hidden = false; } }, onOpen: () => { // If offlineElement exists, hide it and show the mountElement - if (this.offlineElement) { + if (this.offlineElement && this.mountElement) { 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/components.ts b/src/js/src/components.ts new file mode 100644 index 00000000..176a1f30 --- /dev/null +++ b/src/js/src/components.ts @@ -0,0 +1,88 @@ +import { DjangoFormProps, HttpRequestProps } from "./types"; +import React from "react"; +import ReactDOM from "react-dom"; +/** + * Interface used to bind a ReactPy node to React. + */ +export function bind(node) { + return { + create: (type, props, children) => + React.createElement(type, props, ...children), + render: (element) => { + ReactDOM.render(element, node); + }, + unmount: () => ReactDOM.unmountComponentAtNode(node), + }; +} + +export function DjangoForm({ + onSubmitCallback, + formId, +}: DjangoFormProps): null { + React.useEffect(() => { + const form = document.getElementById(formId) as HTMLFormElement; + + // Submission event function + const onSubmitEvent = (event) => { + event.preventDefault(); + const formData = new FormData(form); + + // Convert the FormData object to a plain object by iterating through it + // If duplicate keys are present, convert the value into an array of values + const entries = formData.entries(); + const formDataArray = Array.from(entries); + const formDataObject = formDataArray.reduce((acc, [key, value]) => { + if (acc[key]) { + if (Array.isArray(acc[key])) { + acc[key].push(value); + } else { + acc[key] = [acc[key], value]; + } + } else { + acc[key] = value; + } + return acc; + }, {}); + + onSubmitCallback(formDataObject); + }; + + // Bind the event listener + if (form) { + form.addEventListener("submit", onSubmitEvent); + } + + // Unbind the event listener when the component dismounts + return () => { + if (form) { + form.removeEventListener("submit", onSubmitEvent); + } + }; + }, []); + + return null; +} + +export function HttpRequest({ method, url, body, callback }: HttpRequestProps) { + React.useEffect(() => { + fetch(url, { + method: method, + body: body, + }) + .then((response) => { + response + .text() + .then((text) => { + callback(response.status, text); + }) + .catch(() => { + callback(response.status, ""); + }); + }) + .catch(() => { + callback(520, ""); + }); + }, []); + + return null; +} diff --git a/src/js/src/index.ts b/src/js/src/index.ts new file mode 100644 index 00000000..01856c7d --- /dev/null +++ b/src/js/src/index.ts @@ -0,0 +1,2 @@ +export { HttpRequest, DjangoForm, bind } from "./components"; +export { mountComponent } from "./mount"; diff --git a/src/js/src/index.tsx b/src/js/src/mount.tsx similarity index 93% rename from src/js/src/index.tsx rename to src/js/src/mount.tsx index 23300874..a3a02087 100644 --- a/src/js/src/index.tsx +++ b/src/js/src/mount.tsx @@ -1,6 +1,6 @@ import { ReactPyDjangoClient } from "./client"; import React from "react"; -import { render } from "react-dom"; +import ReactDOM from "react-dom"; import { Layout } from "@reactpy/client/src/components"; export function mountComponent( @@ -52,7 +52,6 @@ export function mountComponent( const client = new ReactPyDjangoClient({ urls: { componentUrl: componentUrl, - query: document.location.search, jsModules: `${httpOrigin}/${jsModulesPath}`, }, reconnectOptions: { @@ -69,7 +68,7 @@ export function mountComponent( // 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) { + if (client.prerenderElement && client.mountElement) { client.prerenderElement.replaceWith(client.mountElement); client.prerenderElement = null; } @@ -77,5 +76,5 @@ export function mountComponent( } // Start rendering the component - render(, client.mountElement); + ReactDOM.render(, client.mountElement); } diff --git a/src/js/src/types.ts b/src/js/src/types.ts index eea8a866..e3be73d7 100644 --- a/src/js/src/types.ts +++ b/src/js/src/types.ts @@ -7,7 +7,6 @@ export type ReconnectOptions = { export type ReactPyUrls = { componentUrl: URL; - query: string; jsModules: string; }; @@ -18,3 +17,15 @@ export type ReactPyDjangoClientProps = { prerenderElement: HTMLElement | null; offlineElement: HTMLElement | null; }; + +export interface DjangoFormProps { + onSubmitCallback: (data: Object) => void; + formId: string; +} + +export interface HttpRequestProps { + method: string; + url: string; + body: string; + callback: (status: Number, response: string) => void; +} diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json deleted file mode 100644 index b73afab7..00000000 --- a/src/js/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "esnext", - "moduleResolution": "node", - "jsx": "react", - "allowSyntheticDefaultImports": true, - }, - "paths": { - "react": ["./node_modules/preact/compat/"], - "react-dom": ["./node_modules/preact/compat/"], - }, -} diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index d4ece375..c272c9a8 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -3,7 +3,6 @@ import nest_asyncio from reactpy_django import ( - checks, components, decorators, hooks, @@ -12,23 +11,18 @@ types, utils, ) -from reactpy_django.websocket.paths import ( - REACTPY_WEBSOCKET_PATH, - REACTPY_WEBSOCKET_ROUTE, -) +from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_ROUTE -__version__ = "4.0.0" +__version__ = "5.2.1" __all__ = [ - "REACTPY_WEBSOCKET_PATH", "REACTPY_WEBSOCKET_ROUTE", - "html", - "hooks", "components", "decorators", + "hooks", + "html", + "router", "types", "utils", - "checks", - "router", ] # Fixes bugs with REACTPY_BACKHAUL_THREAD + built-in asyncio event loops. diff --git a/src/reactpy_django/auth/__init__.py b/src/reactpy_django/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/reactpy_django/auth/components.py b/src/reactpy_django/auth/components.py new file mode 100644 index 00000000..e0a1e065 --- /dev/null +++ b/src/reactpy_django/auth/components.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import asyncio +import contextlib +from logging import getLogger +from typing import TYPE_CHECKING, Any +from uuid import uuid4 + +from django.urls import reverse +from reactpy import component, hooks + +from reactpy_django.javascript_components import HttpRequest +from reactpy_django.models import AuthToken + +if TYPE_CHECKING: + from django.contrib.sessions.backends.base import SessionBase + +_logger = getLogger(__name__) + + +@component +def root_manager(child: Any): + """This component is serves as the parent component for any ReactPy component tree, + which allows for the management of the entire component tree.""" + scope = hooks.use_connection().scope + _, set_rerender = hooks.use_state(uuid4) + + @hooks.use_effect(dependencies=[]) + def setup_asgi_scope(): + """Store trigger functions in the websocket scope so that ReactPy-Django's hooks can command + any relevant actions.""" + scope["reactpy"]["rerender"] = rerender + + def rerender(): + """Event that can force a rerender of the entire component tree.""" + set_rerender(uuid4()) + + return child + + +@component +def auth_manager(): + """This component uses a client-side component alongside an authentication token + to make the client (browser) to switch the HTTP auth session, to make it match the websocket session. + + Used to force persistent authentication between Django's websocket and HTTP stack.""" + from reactpy_django import config + + sync_needed, set_sync_needed = hooks.use_state(False) + token = hooks.use_ref("") + scope = hooks.use_connection().scope + + @hooks.use_effect(dependencies=[]) + def setup_asgi_scope(): + """Store trigger functions in the websocket scope so that ReactPy-Django's hooks can command + any relevant actions.""" + scope["reactpy"]["synchronize_auth"] = synchronize_auth + + @hooks.use_effect(dependencies=[sync_needed]) + async def synchronize_auth_watchdog(): + """Detect if the client has taken too long to request a auth session synchronization. + + This effect will automatically be cancelled if the session is successfully + synchronized (via effect dependencies).""" + if sync_needed: + await asyncio.sleep(config.REACTPY_AUTH_TOKEN_MAX_AGE + 0.1) + await asyncio.to_thread( + _logger.warning, + f"Client did not switch authentication sessions within {config.REACTPY_AUTH_TOKEN_MAX_AGE} (REACTPY_AUTH_TOKEN_MAX_AGE) seconds.", + ) + set_sync_needed(False) + + async def synchronize_auth(): + """Event that can command the client to switch HTTP auth sessions (to match the websocket session).""" + session: SessionBase | None = scope.get("session") + if not session or not session.session_key: + return + + # Delete previous token to resolve race conditions where... + # 1. Login was called multiple times before the first one is completed. + # 2. Login was called, but the server failed to respond to the HTTP request. + if token.current: + with contextlib.suppress(AuthToken.DoesNotExist): + obj = await AuthToken.objects.aget(value=token.current) + await obj.adelete() + + # Create a fresh token + token.set_current(str(uuid4())) + + # Begin the process of synchronizing HTTP and websocket auth sessions + obj = await AuthToken.objects.acreate(value=token.current, session_key=session.session_key) + await obj.asave() + set_sync_needed(True) + + async def synchronize_auth_callback(status_code: int, response: str): + """This callback acts as a communication bridge, allowing the client to notify the server + of the status of auth session switch.""" + set_sync_needed(False) + if status_code >= 300 or status_code < 200: + await asyncio.to_thread( + _logger.error, + f"Client returned unexpected HTTP status code ({status_code}) while trying to synchronize authentication sessions.", + ) + + # If needed, synchronize authenication sessions by configuring all relevant session cookies. + # This is achieved by commanding the client to perform a HTTP request to our API endpoint + # that will set any required cookies. + if sync_needed: + return HttpRequest( + { + "method": "GET", + "url": reverse("reactpy:auth_manager", args=[token.current]), + "body": None, + "callback": synchronize_auth_callback, + }, + ) + + return None diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index d836a9ca..88001c77 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -1,6 +1,7 @@ import contextlib import math import sys +from uuid import uuid4 from django.contrib.staticfiles.finders import find from django.core.checks import Error, Tags, Warning, register @@ -17,29 +18,27 @@ def reactpy_warnings(app_configs, **kwargs): from reactpy_django.config import REACTPY_FAILED_COMPONENTS warnings = [] - INSTALLED_APPS: list[str] = getattr(settings, "INSTALLED_APPS", []) + installed_apps: list[str] = getattr(settings, "INSTALLED_APPS", []) - # REACTPY_DATABASE is not an in-memory database. + # Check if REACTPY_DATABASE is not an in-memory database. if ( - getattr(settings, "DATABASES", {}) - .get(getattr(settings, "REACTPY_DATABASE", "default"), {}) - .get("NAME", None) + getattr(settings, "DATABASES", {}).get(getattr(settings, "REACTPY_DATABASE", "default"), {}).get("NAME", None) == ":memory:" ): warnings.append( Warning( - "Using ReactPy with an in-memory database can cause unexpected " - "behaviors.", + "Using ReactPy with an in-memory database can cause unexpected behaviors.", hint="Configure settings.py:DATABASES[REACTPY_DATABASE], to use a " "multiprocessing and thread safe database.", id="reactpy_django.W001", ) ) - # ReactPy URLs exist + # Check if ReactPy URLs are reachable try: reverse("reactpy:web_modules", kwargs={"file": "example"}) reverse("reactpy:view_to_iframe", kwargs={"dotted_path": "example"}) + reverse("reactpy:session_manager", args=[str(uuid4())]) except Exception: warnings.append( Warning( @@ -52,24 +51,22 @@ def reactpy_warnings(app_configs, **kwargs): ) # Warn if REACTPY_BACKHAUL_THREAD is set to True with Daphne - if ( - sys.argv[0].endswith("daphne") - or ("runserver" in sys.argv and "daphne" in INSTALLED_APPS) - ) and getattr(settings, "REACTPY_BACKHAUL_THREAD", False): + if (sys.argv[0].endswith("daphne") or ("runserver" in sys.argv and "daphne" in installed_apps)) and getattr( + settings, "REACTPY_BACKHAUL_THREAD", False + ): warnings.append( Warning( - "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled " - "and you running with Daphne.", + "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled and you running with Daphne.", hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different web server.", id="reactpy_django.W003", ) ) - # Check if reactpy_django/client.js is available - if not find("reactpy_django/client.js"): + # Check if reactpy_django/index.js is available + if not find("reactpy_django/index.js"): warnings.append( Warning( - "ReactPy client.js could not be found within Django static files!", + "ReactPy index.js could not be found within Django static files!", hint="Check all static files related Django settings and INSTALLED_APPS.", id="reactpy_django.W004", ) @@ -79,10 +76,8 @@ def reactpy_warnings(app_configs, **kwargs): if REACTPY_FAILED_COMPONENTS: warnings.append( Warning( - "ReactPy failed to register the following components:\n\t+ " - + "\n\t+ ".join(REACTPY_FAILED_COMPONENTS), - hint="Check if these paths are valid, or if an exception is being " - "raised during import.", + "ReactPy failed to register the following components:\n\t+ " + "\n\t+ ".join(REACTPY_FAILED_COMPONENTS), + hint="Check if these paths are valid, or if an exception is being raised during import.", id="reactpy_django.W005", ) ) @@ -102,23 +97,12 @@ def reactpy_warnings(app_configs, **kwargs): # DELETED W007: Check if REACTPY_WEBSOCKET_URL doesn't end with a slash # DELETED W008: Check if REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character - - # Removed REACTPY_WEBSOCKET_URL setting - if getattr(settings, "REACTPY_WEBSOCKET_URL", None): - warnings.append( - Warning( - "REACTPY_WEBSOCKET_URL has been removed.", - hint="Use REACTPY_URL_PREFIX instead.", - id="reactpy_django.W009", - ) - ) + # DELETED W009: Check if deprecated value REACTPY_WEBSOCKET_URL exists # Check if REACTPY_URL_PREFIX is being used properly in our HTTP URLs with contextlib.suppress(NoReverseMatch): - full_path = reverse("reactpy:web_modules", kwargs={"file": "example"}).strip( - "/" - ) - reactpy_http_prefix = f'{full_path[: full_path.find("web_module/")].strip("/")}' + full_path = reverse("reactpy:web_modules", kwargs={"file": "example"}).strip("/") + reactpy_http_prefix = f"{full_path[: full_path.find('web_module/')].strip('/')}" if reactpy_http_prefix != config.REACTPY_URL_PREFIX: warnings.append( Warning( @@ -147,31 +131,20 @@ def reactpy_warnings(app_configs, **kwargs): ) # Check if `daphne` is not in installed apps when using `runserver` - if "runserver" in sys.argv and "daphne" not in getattr( - settings, "INSTALLED_APPS", [] - ): + if "runserver" in sys.argv and "daphne" not in getattr(settings, "INSTALLED_APPS", []): warnings.append( Warning( - "You have not configured runserver to use ASGI.", + "You have not configured the `runserver` command to use ASGI. " + "ReactPy will work properly in this configuration.", hint="Add daphne to settings.py:INSTALLED_APPS.", id="reactpy_django.W012", ) ) - # Removed REACTPY_RECONNECT_MAX setting - if getattr(settings, "REACTPY_RECONNECT_MAX", None): - warnings.append( - Warning( - "REACTPY_RECONNECT_MAX has been removed.", - hint="See the docs for the new REACTPY_RECONNECT_* settings.", - id="reactpy_django.W013", - ) - ) + # DELETED W013: Check if deprecated value REACTPY_RECONNECT_MAX exists - if ( - isinstance(config.REACTPY_RECONNECT_INTERVAL, int) - and config.REACTPY_RECONNECT_INTERVAL > 30000 - ): + # Check if REACTPY_RECONNECT_INTERVAL is set to a large value + if isinstance(config.REACTPY_RECONNECT_INTERVAL, int) and config.REACTPY_RECONNECT_INTERVAL > 30000: warnings.append( Warning( "REACTPY_RECONNECT_INTERVAL is set to >30 seconds. Are you sure this is intentional? " @@ -181,20 +154,19 @@ def reactpy_warnings(app_configs, **kwargs): ) ) - if ( - isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) - and config.REACTPY_RECONNECT_MAX_RETRIES > 5000 - ): + # Check if REACTPY_RECONNECT_MAX_RETRIES is set to a large value + if isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) and config.REACTPY_RECONNECT_MAX_RETRIES > 5000: warnings.append( Warning( - "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value. Are you sure this is intentional? " + "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value " + f"{config.REACTPY_RECONNECT_MAX_RETRIES}. Are you sure this is intentional? " "This may leave your clients attempting reconnections for a long time.", hint="Check your value for REACTPY_RECONNECT_MAX_RETRIES or suppress this warning.", id="reactpy_django.W015", ) ) - # Check if the value is too large (greater than 50) + # Check if the REACTPY_RECONNECT_BACKOFF_MULTIPLIER is set to a large value if ( isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)) and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 100 @@ -207,6 +179,7 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is reachable if ( isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and isinstance(config.REACTPY_RECONNECT_INTERVAL, int) @@ -216,18 +189,12 @@ def reactpy_warnings(app_configs, **kwargs): and config.REACTPY_RECONNECT_MAX_INTERVAL > 0 and config.REACTPY_RECONNECT_MAX_RETRIES > 0 and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 1 - and ( - config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER - ** config.REACTPY_RECONNECT_MAX_RETRIES - ) + and (config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER**config.REACTPY_RECONNECT_MAX_RETRIES) * config.REACTPY_RECONNECT_INTERVAL < config.REACTPY_RECONNECT_MAX_INTERVAL ): max_value = math.floor( - ( - config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER - ** config.REACTPY_RECONNECT_MAX_RETRIES - ) + (config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER**config.REACTPY_RECONNECT_MAX_RETRIES) * config.REACTPY_RECONNECT_INTERVAL ) warnings.append( @@ -239,14 +206,12 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if 'reactpy_django' is in the correct position in INSTALLED_APPS position_to_beat = 0 - for app in INSTALLED_APPS: + for app in installed_apps: if app.startswith("django.contrib."): - position_to_beat = INSTALLED_APPS.index(app) - if ( - "reactpy_django" in INSTALLED_APPS - and INSTALLED_APPS.index("reactpy_django") < position_to_beat - ): + position_to_beat = installed_apps.index(app) + if "reactpy_django" in installed_apps and installed_apps.index("reactpy_django") < position_to_beat: warnings.append( Warning( "The position of 'reactpy_django' in INSTALLED_APPS is suspicious.", @@ -255,6 +220,7 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if user misspelled REACTPY_CLEAN_SESSIONS if getattr(settings, "REACTPY_CLEAN_SESSION", None): warnings.append( Warning( @@ -264,6 +230,27 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if REACTPY_AUTH_TOKEN_MAX_AGE is a large value + auth_token_timeout = config.REACTPY_AUTH_TOKEN_MAX_AGE + if isinstance(auth_token_timeout, int) and auth_token_timeout > 120: + warnings.append( + Warning( + "REACTPY_AUTH_TOKEN_MAX_AGE is set to a very large value.", + hint="It is suggested to keep REACTPY_AUTH_TOKEN_MAX_AGE under 120 seconds to prevent security risks.", + id="reactpy_django.W020", + ) + ) + + # Check if REACTPY_AUTH_TOKEN_MAX_AGE is a small value + if isinstance(auth_token_timeout, int) and auth_token_timeout <= 2: + warnings.append( + Warning( + "REACTPY_AUTH_TOKEN_MAX_AGE is set to a very low value.", + hint="It is suggested to keep REACTPY_AUTH_TOKEN_MAX_AGE above 2 seconds to account for client and server latency.", + id="reactpy_django.W021", + ) + ) + return warnings @@ -286,22 +273,18 @@ def reactpy_errors(app_configs, **kwargs): ) # DATABASE_ROUTERS is properly configured when REACTPY_DATABASE is defined - if getattr( - settings, "REACTPY_DATABASE", None - ) and "reactpy_django.database.Router" not in getattr( + if getattr(settings, "REACTPY_DATABASE", None) and "reactpy_django.database.Router" not in getattr( settings, "DATABASE_ROUTERS", [] ): errors.append( Error( - "ReactPy database has been changed but the database router is " - "not configured.", - hint="Set settings.py:DATABASE_ROUTERS to " - "['reactpy_django.database.Router', ...]", + "ReactPy database has been changed but the database router is not configured.", + hint="Set settings.py:DATABASE_ROUTERS to ['reactpy_django.database.Router', ...]", id="reactpy_django.E002", ) ) - # All settings in reactpy_django.conf are the correct data type + # Check if REACTPY_URL_PREFIX is a valid data type if not isinstance(getattr(settings, "REACTPY_URL_PREFIX", ""), str): errors.append( Error( @@ -311,6 +294,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E003", ) ) + + # Check if REACTPY_SESSION_MAX_AGE is a valid data type if not isinstance(getattr(settings, "REACTPY_SESSION_MAX_AGE", 0), int): errors.append( Error( @@ -320,6 +305,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E004", ) ) + + # Check if REACTPY_CACHE is a valid data type if not isinstance(getattr(settings, "REACTPY_CACHE", ""), str): errors.append( Error( @@ -329,6 +316,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E005", ) ) + + # Check if REACTPY_DATABASE is a valid data type if not isinstance(getattr(settings, "REACTPY_DATABASE", ""), str): errors.append( Error( @@ -338,9 +327,9 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E006", ) ) - if not isinstance( - getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None)) - ): + + # Check if REACTPY_DEFAULT_QUERY_POSTPROCESSOR is a valid data type + if not isinstance(getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None))): errors.append( Error( "Invalid type for REACTPY_DEFAULT_QUERY_POSTPROCESSOR.", @@ -349,6 +338,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E007", ) ) + + # Check if REACTPY_AUTH_BACKEND is a valid data type if not isinstance(getattr(settings, "REACTPY_AUTH_BACKEND", ""), str): errors.append( Error( @@ -361,6 +352,7 @@ def reactpy_errors(app_configs, **kwargs): # DELETED E009: Check if `channels` is in INSTALLED_APPS + # Check if REACTPY_DEFAULT_HOSTS is a valid data type if not isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", []), list): errors.append( Error( @@ -371,7 +363,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) - # Check of all values in the list are strings + # Check of all values in the REACTPY_DEFAULT_HOSTS are strings if isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", None), list): for host in settings.REACTPY_DEFAULT_HOSTS: if not isinstance(host, str): @@ -385,6 +377,7 @@ def reactpy_errors(app_configs, **kwargs): ) break + # Check if REACTPY_RECONNECT_INTERVAL is a valid data type if not isinstance(config.REACTPY_RECONNECT_INTERVAL, int): errors.append( Error( @@ -394,10 +387,8 @@ def reactpy_errors(app_configs, **kwargs): ) ) - if ( - isinstance(config.REACTPY_RECONNECT_INTERVAL, int) - and config.REACTPY_RECONNECT_INTERVAL < 0 - ): + # Check if REACTPY_RECONNECT_INTERVAL is a positive integer + if isinstance(config.REACTPY_RECONNECT_INTERVAL, int) and config.REACTPY_RECONNECT_INTERVAL < 0: errors.append( Error( "Invalid value for REACTPY_RECONNECT_INTERVAL.", @@ -406,6 +397,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is a valid data type if not isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int): errors.append( Error( @@ -415,10 +407,8 @@ def reactpy_errors(app_configs, **kwargs): ) ) - if ( - isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) - and config.REACTPY_RECONNECT_MAX_INTERVAL < 0 - ): + # Check if REACTPY_RECONNECT_MAX_INTERVAL is a positive integer + if isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and config.REACTPY_RECONNECT_MAX_INTERVAL < 0: errors.append( Error( "Invalid value for REACTPY_RECONNECT_MAX_INTERVAL.", @@ -427,6 +417,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is greater than REACTPY_RECONNECT_INTERVAL if ( isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and isinstance(config.REACTPY_RECONNECT_INTERVAL, int) @@ -440,6 +431,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_RETRIES is a valid data type if not isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int): errors.append( Error( @@ -449,10 +441,8 @@ def reactpy_errors(app_configs, **kwargs): ) ) - if ( - isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) - and config.REACTPY_RECONNECT_MAX_RETRIES < 0 - ): + # Check if REACTPY_RECONNECT_MAX_RETRIES is a positive integer + if isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) and config.REACTPY_RECONNECT_MAX_RETRIES < 0: errors.append( Error( "Invalid value for REACTPY_RECONNECT_MAX_RETRIES.", @@ -461,6 +451,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_BACKOFF_MULTIPLIER is a valid data type if not isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)): errors.append( Error( @@ -470,6 +461,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_BACKOFF_MULTIPLIER is greater than or equal to 1 if ( isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)) and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER < 1 @@ -482,6 +474,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_PRERENDER is a valid data type if not isinstance(config.REACTPY_PRERENDER, bool): errors.append( Error( @@ -491,6 +484,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_AUTO_RELOGIN is a valid data type if not isinstance(config.REACTPY_AUTO_RELOGIN, bool): errors.append( Error( @@ -500,6 +494,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_INTERVAL is a valid data type if not isinstance(config.REACTPY_CLEAN_INTERVAL, (int, type(None))): errors.append( Error( @@ -509,10 +504,8 @@ def reactpy_errors(app_configs, **kwargs): ) ) - if ( - isinstance(config.REACTPY_CLEAN_INTERVAL, int) - and config.REACTPY_CLEAN_INTERVAL < 0 - ): + # Check if REACTPY_CLEAN_INTERVAL is a positive integer + if isinstance(config.REACTPY_CLEAN_INTERVAL, int) and config.REACTPY_CLEAN_INTERVAL < 0: errors.append( Error( "Invalid value for REACTPY_CLEAN_INTERVAL.", @@ -521,6 +514,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_SESSIONS is a valid data type if not isinstance(config.REACTPY_CLEAN_SESSIONS, bool): errors.append( Error( @@ -530,6 +524,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_USER_DATA is a valid data type if not isinstance(config.REACTPY_CLEAN_USER_DATA, bool): errors.append( Error( @@ -539,4 +534,34 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_AUTH_TOKENS is a valid data type + if not isinstance(config.REACTPY_CLEAN_AUTH_TOKENS, bool): + errors.append( + Error( + "Invalid type for REACTPY_CLEAN_AUTH_TOKENS.", + hint="REACTPY_CLEAN_AUTH_TOKENS should be a boolean.", + id="reactpy_django.E027", + ) + ) + + # Check if REACTPY_AUTH_TOKEN_MAX_AGE is a valid data type + if not isinstance(config.REACTPY_AUTH_TOKEN_MAX_AGE, int): + errors.append( + Error( + "Invalid type for REACTPY_AUTH_TOKEN_MAX_AGE.", + hint="REACTPY_AUTH_TOKEN_MAX_AGE should be an integer.", + id="reactpy_django.E028", + ) + ) + + # Check if REACTPY_AUTH_TOKEN_MAX_AGE is a positive integer + if isinstance(config.REACTPY_AUTH_TOKEN_MAX_AGE, int) and config.REACTPY_AUTH_TOKEN_MAX_AGE < 0: + errors.append( + Error( + "Invalid value for REACTPY_AUTH_TOKEN_MAX_AGE.", + hint="REACTPY_AUTH_TOKEN_MAX_AGE should be a non-negative integer.", + id="reactpy_django.E029", + ) + ) + return errors diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 579c73e3..9234c42e 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -1,65 +1,45 @@ +"""This file contains Django related components. Most of these components utilize wrappers to fix type hints.""" + from __future__ import annotations import json -import os -from typing import Any, Callable, Sequence, Union, cast, overload +from typing import TYPE_CHECKING, Any, Callable, Union, cast from urllib.parse import urlencode -from uuid import uuid4 -from warnings import warn -from django.contrib.staticfiles.finders import find -from django.core.cache import caches from django.http import HttpRequest from django.urls import reverse -from django.views import View from reactpy import component, hooks, html, utils from reactpy.types import ComponentType, Key, VdomDict from reactpy_django.exceptions import ViewNotRegisteredError -from reactpy_django.html import pyscript +from reactpy_django.forms.components import _django_form +from reactpy_django.pyscript.components import _pyscript_component from reactpy_django.utils import ( + cached_static_file, + del_html_head_body_transform, generate_obj_name, import_module, - render_pyscript_template, render_view, - vdom_or_component_to_string, ) +if TYPE_CHECKING: + from collections.abc import Sequence -# Type hints for: -# 1. example = view_to_component(my_view, ...) -# 2. @view_to_component -@overload -def view_to_component( - view: Callable | View | str, - compatibility: bool = False, - transforms: Sequence[Callable[[VdomDict], Any]] = (), - strict_parsing: bool = True, -) -> Any: ... - + from django.forms import Form, ModelForm + from django.views import View -# Type hints for: -# 1. @view_to_component(...) -@overload -def view_to_component( - view: None = ..., - compatibility: bool = False, - transforms: Sequence[Callable[[VdomDict], Any]] = (), - strict_parsing: bool = True, -) -> Callable[[Callable], Any]: ... + from reactpy_django.types import AsyncFormEvent, SyncFormEvent, ViewToComponentConstructor, ViewToIframeConstructor def view_to_component( - view: Callable | View | str | None = None, - compatibility: bool = False, + view: Callable | View | str, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Any | Callable[[Callable], Any]: +) -> ViewToComponentConstructor: """Converts a Django view to a ReactPy component. Keyword Args: view: The view to convert, or the view's dotted path as a string. - compatibility: **DEPRECATED.** Use `view_to_iframe` instead. transforms: A list of functions that transforms the newly generated VDOM. \ The functions will be called on each VDOM node. strict_parsing: If True, an exception will be generated if the HTML does not \ @@ -69,42 +49,26 @@ def view_to_component( A function that takes `request, *args, key, **kwargs` and returns a ReactPy component. """ - def decorator(view: Callable | View | str): - if not view: - raise ValueError("A view must be provided to `view_to_component`") - - def constructor( - request: HttpRequest | None = None, - *args, - key: Key | None = None, - **kwargs, - ): - return _view_to_component( - view=view, - compatibility=compatibility, - transforms=transforms, - strict_parsing=strict_parsing, - request=request, - args=args, - kwargs=kwargs, - key=key, - ) - - return constructor - - if not view: - warn( - "Using `view_to_component` as a decorator is deprecated. " - "This functionality will be removed in a future version.", - DeprecationWarning, + def constructor( + request: HttpRequest | None = None, + *args, + key: Key | None = None, + **kwargs, + ) -> ComponentType: + return _view_to_component( + view=view, + transforms=transforms, + strict_parsing=strict_parsing, + request=request, + args=args, + kwargs=kwargs, + key=key, ) - return decorator(view) if view else decorator + return constructor -def view_to_iframe( - view: Callable | View | str, extra_props: dict[str, Any] | None = None -): +def view_to_iframe(view: Callable | View | str, extra_props: dict[str, Any] | None = None) -> ViewToIframeConstructor: """ Args: view: The view function or class to convert, or the dotted path to the view. @@ -120,15 +84,13 @@ def constructor( *args, key: Key | None = None, **kwargs, - ): - return _view_to_iframe( - view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key - ) + ) -> ComponentType: + return _view_to_iframe(view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key) return constructor -def django_css(static_path: str, key: Key | None = None): +def django_css(static_path: str, key: Key | None = None) -> ComponentType: """Fetches a CSS static file for use within ReactPy. This allows for deferred CSS loading. Args: @@ -141,7 +103,7 @@ def django_css(static_path: str, key: Key | None = None): return _django_css(static_path=static_path, key=key) -def django_js(static_path: str, key: Key | None = None): +def django_js(static_path: str, key: Key | None = None) -> ComponentType: """Fetches a JS static file for use within ReactPy. This allows for deferred JS loading. Args: @@ -154,11 +116,69 @@ def django_js(static_path: str, key: Key | None = None): return _django_js(static_path=static_path, key=key) +def django_form( + form: type[Form | ModelForm], + *, + on_success: AsyncFormEvent | SyncFormEvent | None = None, + on_error: AsyncFormEvent | SyncFormEvent | None = None, + on_receive_data: AsyncFormEvent | SyncFormEvent | None = None, + on_change: AsyncFormEvent | SyncFormEvent | None = None, + auto_save: bool = True, + extra_props: dict[str, Any] | None = None, + extra_transforms: Sequence[Callable[[VdomDict], Any]] | None = None, + form_template: str | None = None, + thread_sensitive: bool = True, + top_children: Sequence[Any] = (), + bottom_children: Sequence[Any] = (), + key: Key | None = None, +) -> ComponentType: + """Converts a Django form to a ReactPy component. + + Args: + form: The form to convert. + + Keyword Args: + on_success: A callback function that is called when the form is successfully submitted. + on_error: A callback function that is called when the form submission fails. + on_receive_data: A callback function that is called before newly submitted form data is rendered. + on_change: A callback function that is called when a form field is modified by the user. + auto_save: If `True`, the form will automatically call `save` on successful submission of \ + a `ModelForm`. This has no effect on regular `Form` instances. + extra_props: Additional properties to add to the `html.form` element. + extra_transforms: A list of functions that transforms the newly generated VDOM. \ + The functions will be repeatedly called on each VDOM node. + form_template: The template to use for the form. If `None`, Django's default template is used. + thread_sensitive: Whether to run event callback functions in thread sensitive mode. \ + This mode only applies to sync functions, and is turned on by default due to Django \ + ORM limitations. + top_children: Additional elements to add to the top of the form. + bottom_children: Additional elements to add to the bottom of the form. + key: A key to uniquely identify this component which is unique amongst a component's \ + immediate siblings. + """ + + return _django_form( + form=form, + on_success=on_success, + on_error=on_error, + on_receive_data=on_receive_data, + on_change=on_change, + auto_save=auto_save, + extra_props=extra_props or {}, + extra_transforms=extra_transforms or [], + form_template=form_template, + thread_sensitive=thread_sensitive, + top_children=top_children, + bottom_children=bottom_children, + key=key, + ) + + def pyscript_component( *file_paths: str, initial: str | VdomDict | ComponentType = "", root: str = "root", -): +) -> ComponentType: """ Args: file_paths: File path to your client-side component. If multiple paths are \ @@ -180,17 +200,13 @@ def pyscript_component( @component def _view_to_component( view: Callable | View | str, - compatibility: bool, transforms: Sequence[Callable[[VdomDict], Any]], strict_parsing: bool, request: HttpRequest | None, args: Sequence | None, kwargs: dict | None, ): - """The actual component. Used to prevent pollution of acceptable kwargs keys.""" - converted_view, set_converted_view = hooks.use_state( - cast(Union[VdomDict, None], None) - ) + converted_view, set_converted_view = hooks.use_state(cast(Union[VdomDict, None], None)) _args: Sequence = args or () _kwargs: dict = kwargs or {} if request: @@ -198,43 +214,28 @@ def _view_to_component( else: _request = HttpRequest() _request.method = "GET" - resolved_view: Callable = import_module(view) if isinstance(view, str) else view # type: ignore[assignment] + resolved_view: Callable = import_module(view) if isinstance(view, str) else view # type: ignore # Render the view render within a hook @hooks.use_effect( dependencies=[ - json.dumps(vars(_request), default=lambda x: generate_obj_name(x)), - json.dumps([_args, _kwargs], default=lambda x: generate_obj_name(x)), + json.dumps(vars(_request), default=generate_obj_name), + json.dumps([_args, _kwargs], default=generate_obj_name), ] ) - async def async_render(): + async def _render_view(): """Render the view in an async hook to avoid blocking the main thread.""" - # Compatibility mode doesn't require a traditional render - if compatibility: - return - # Render the view response = await render_view(resolved_view, _request, _args, _kwargs) set_converted_view( utils.html_to_vdom( response.content.decode("utf-8").strip(), - utils.del_html_head_body_transform, + del_html_head_body_transform, *transforms, strict=strict_parsing, ) ) - # Render in compatibility mode, if needed - if compatibility: - # Warn the user that compatibility mode is deprecated - warn( - "view_to_component(compatibility=True) is deprecated and will be removed in a future version. " - "Please use `view_to_iframe` instead.", - DeprecationWarning, - ) - - return view_to_iframe(resolved_view)(*_args, **_kwargs) - # Return the view if it's been rendered via the `async_render` hook return converted_view @@ -245,20 +246,20 @@ def _view_to_iframe( extra_props: dict[str, Any] | None, args: Sequence, kwargs: dict, -) -> VdomDict: - """The actual component. Used to prevent pollution of acceptable kwargs keys.""" +): from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS if hasattr(view, "view_class"): - view = view.view_class + view = view.view_class # type: ignore dotted_path = view if isinstance(view, str) else generate_obj_name(view) registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path) if not registered_view: - raise ViewNotRegisteredError( + msg = ( f"'{dotted_path}' has not been registered as an iframe! " "Are you sure you called `register_iframe` within a Django `AppConfig.ready` method?" ) + raise ViewNotRegisteredError(msg) query = kwargs.copy() if args: @@ -271,7 +272,6 @@ def _view_to_iframe( { "src": reverse("reactpy:view_to_iframe", args=[dotted_path]) + query_string, "style": {"border": "none"}, - "onload": 'javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";}(this));', "loading": "lazy", } | extra_props @@ -280,62 +280,9 @@ def _view_to_iframe( @component def _django_css(static_path: str): - return html.style(_cached_static_contents(static_path)) + return html.style(cached_static_file(static_path)) @component def _django_js(static_path: str): - return html.script(_cached_static_contents(static_path)) - - -def _cached_static_contents(static_path: str) -> str: - from reactpy_django.config import REACTPY_CACHE - - # Try to find the file within Django's static files - abs_path = find(static_path) - if not abs_path: - raise FileNotFoundError( - f"Could not find static file {static_path} within Django's static files." - ) - - # Fetch the file from cache, if available - last_modified_time = os.stat(abs_path).st_mtime - cache_key = f"reactpy_django:static_contents:{static_path}" - file_contents: str | None = caches[REACTPY_CACHE].get( - cache_key, version=int(last_modified_time) - ) - if file_contents is None: - with open(abs_path, encoding="utf-8") as static_file: - file_contents = static_file.read() - caches[REACTPY_CACHE].delete(cache_key) - caches[REACTPY_CACHE].set( - cache_key, file_contents, timeout=None, version=int(last_modified_time) - ) - - return file_contents - - -@component -def _pyscript_component( - *file_paths: str, - initial: str | VdomDict | ComponentType = "", - root: str = "root", -): - rendered, set_rendered = hooks.use_state(False) - uuid = uuid4().hex.replace("-", "") - initial = vdom_or_component_to_string(initial, uuid=uuid) - executor = render_pyscript_template(file_paths, uuid, root) - - if not rendered: - # FIXME: This is needed to properly re-render PyScript during a WebSocket - # disconnection / reconnection. There may be a better way to do this in the future. - set_rendered(True) - return None - - return html._( - html.div( - {"id": f"pyscript-{uuid}", "className": "pyscript", "data-uuid": uuid}, - initial, - ), - pyscript({"async": ""}, executor), - ) + return html.script(cached_static_file(static_path)) diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 21a30a32..cc3ca2fc 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -1,47 +1,49 @@ from __future__ import annotations from itertools import cycle -from typing import Callable +from typing import TYPE_CHECKING, Callable from django.conf import settings from django.core.cache import DEFAULT_CACHE_ALIAS from django.db import DEFAULT_DB_ALIAS -from django.views import View +from reactpy.config import REACTPY_ASYNC_RENDERING as _REACTPY_ASYNC_RENDERING from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE -from reactpy.core.types import ComponentConstructor -from reactpy_django.types import ( - AsyncPostprocessor, - SyncPostprocessor, -) from reactpy_django.utils import import_dotted_path +if TYPE_CHECKING: + from django.views import View + from reactpy.core.types import ComponentConstructor + + from reactpy_django.types import ( + AsyncPostprocessor, + SyncPostprocessor, + ) + # Non-configurable values -_REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) -REACTPY_DEBUG_MODE = _REACTPY_DEBUG_MODE.current REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {} REACTPY_FAILED_COMPONENTS: set[str] = set() REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {} - -# Remove in a future release -REACTPY_WEBSOCKET_URL = getattr( - settings, - "REACTPY_WEBSOCKET_URL", - "reactpy/", -) - # Configurable through Django settings.py +DJANGO_DEBUG = settings.DEBUG # Snapshot of Django's DEBUG setting +_REACTPY_DEBUG_MODE.set_current(settings.DEBUG) +_REACTPY_ASYNC_RENDERING.set_current(getattr(settings, "REACTPY_ASYNC_RENDERING", _REACTPY_ASYNC_RENDERING.current)) REACTPY_URL_PREFIX: str = getattr( settings, "REACTPY_URL_PREFIX", - REACTPY_WEBSOCKET_URL, + "reactpy/", ).strip("/") REACTPY_SESSION_MAX_AGE: int = getattr( settings, "REACTPY_SESSION_MAX_AGE", 259200, # Default to 3 days ) +REACTPY_AUTH_TOKEN_MAX_AGE: int = getattr( + settings, + "REACTPY_AUTH_TOKEN_MAX_AGE", + 30, # Default to 30 seconds +) REACTPY_CACHE: str = getattr( settings, "REACTPY_CACHE", @@ -63,10 +65,7 @@ else: REACTPY_DEFAULT_QUERY_POSTPROCESSOR = import_dotted_path( "reactpy_django.utils.django_query_postprocessor" - if ( - _default_query_postprocessor == "UNSET" - or not isinstance(_default_query_postprocessor, str) - ) + if (_default_query_postprocessor == "UNSET" or not isinstance(_default_query_postprocessor, str)) else _default_query_postprocessor ) REACTPY_AUTH_BACKEND: str | None = getattr( @@ -85,9 +84,7 @@ None, ) REACTPY_DEFAULT_HOSTS: cycle[str] | None = ( - cycle([host.strip("/") for host in _default_hosts if isinstance(host, str)]) - if _default_hosts - else None + cycle([host.strip("/") for host in _default_hosts if isinstance(host, str)]) if _default_hosts else None ) REACTPY_RECONNECT_INTERVAL: int = getattr( settings, @@ -129,8 +126,18 @@ "REACTPY_CLEAN_SESSIONS", True, ) +REACTPY_CLEAN_AUTH_TOKENS: bool = getattr( + settings, + "REACTPY_CLEAN_AUTH_TOKENS", + True, +) REACTPY_CLEAN_USER_DATA: bool = getattr( settings, "REACTPY_CLEAN_USER_DATA", True, ) +REACTPY_DEFAULT_FORM_TEMPLATE: str | None = getattr( + settings, + "REACTPY_DEFAULT_FORM_TEMPLATE", + None, +) diff --git a/src/reactpy_django/database.py b/src/reactpy_django/database.py index 2a7f826d..2acd673e 100644 --- a/src/reactpy_django/database.py +++ b/src/reactpy_django/database.py @@ -1,3 +1,5 @@ +from typing import ClassVar + from reactpy_django.config import REACTPY_DATABASE @@ -7,25 +9,26 @@ class Router: auth and contenttypes applications. """ - route_app_labels = {"reactpy_django"} + route_app_labels: ClassVar[set[str]] = {"reactpy_django"} def db_for_read(self, model, **hints): """Attempts to read go to REACTPY_DATABASE.""" if model._meta.app_label in self.route_app_labels: return REACTPY_DATABASE + return None def db_for_write(self, model, **hints): """Attempts to write go to REACTPY_DATABASE.""" if model._meta.app_label in self.route_app_labels: return REACTPY_DATABASE + return None def allow_relation(self, obj1, obj2, **hints): """Returning `None` only allow relations within the same database. - https://docs.djangoproject.com/en/dev/topics/db/multi-db/#limitations-of-multiple-databases + https://docs.djangoproject.com/en/stable/topics/db/multi-db/#limitations-of-multiple-databases """ return None def allow_migrate(self, db, app_label, model_name=None, **hints): """Make sure ReactPy models only appear in REACTPY_DATABASE.""" - if app_label in self.route_app_labels: - return db == REACTPY_DATABASE + return db == REACTPY_DATABASE if app_label in self.route_app_labels else None diff --git a/src/reactpy_django/decorators.py b/src/reactpy_django/decorators.py index 59c110b3..6b3d220e 100644 --- a/src/reactpy_django/decorators.py +++ b/src/reactpy_django/decorators.py @@ -2,53 +2,15 @@ from functools import wraps from typing import TYPE_CHECKING, Any, Callable -from warnings import warn from reactpy import component -from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict from reactpy_django.exceptions import DecoratorParamError -from reactpy_django.hooks import use_scope, use_user +from reactpy_django.hooks import use_user if TYPE_CHECKING: from django.contrib.auth.models import AbstractUser - - -def auth_required( - component: Callable | None = None, - auth_attribute: str = "is_active", - fallback: ComponentType | Callable | VdomDict | None = None, -) -> Callable: - """If the user passes authentication criteria, the decorated component will be rendered. - Otherwise, the fallback component will be rendered. - - This decorator can be used with or without parentheses. - - Args: - auth_attribute: The value to check within the user object. \ - This is checked in the form of `UserModel.`. \ - fallback: The component or VDOM (`reactpy.html` snippet) to render if the user is not authenticated. - """ - - 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(lambda user: user.is_active).", - DeprecationWarning, - ) - - def decorator(component): - @wraps(component) - def _wrapped_func(*args, **kwargs): - scope = use_scope() - - if getattr(scope["user"], auth_attribute): - return component(*args, **kwargs) - return fallback(*args, **kwargs) if callable(fallback) else fallback - - return _wrapped_func - - # Return for @authenticated(...) and @authenticated respectively - return decorator if component is None else decorator(component) + from reactpy.core.types import ComponentConstructor def user_passes_test( @@ -69,16 +31,14 @@ def user_passes_test( def decorator(user_component): @wraps(user_component) def _wrapper(*args, **kwargs): - return _user_passes_test( - user_component, fallback, test_func, *args, **kwargs - ) + return _user_passes_test(user_component, fallback, test_func, *args, **kwargs) return _wrapper - return decorator + return decorator # type: ignore -@component +@component # type: ignore def _user_passes_test(component_constructor, fallback, test_func, *args, **kwargs): """Dedicated component for `user_passes_test` to allow us to always have access to hooks.""" user = use_user() @@ -87,15 +47,14 @@ def _user_passes_test(component_constructor, fallback, test_func, *args, **kwarg # Ensure that the component is a ReactPy component. user_component = component_constructor(*args, **kwargs) if not getattr(user_component, "render", None): - raise DecoratorParamError( + msg = ( "`user_passes_test` is not decorating a ReactPy component. " "Did you forget `@user_passes_test` must be ABOVE the `@component` decorator?" ) + raise DecoratorParamError(msg) # Render the component. return user_component - # Render the fallback component. - # Returns an empty string if fallback is None, since ReactPy currently renders None as a string. - # TODO: Remove this fallback when ReactPy can render None properly. - return fallback(*args, **kwargs) if callable(fallback) else (fallback or "") + # Render the fallback content. + return fallback(*args, **kwargs) if callable(fallback) else (fallback or None) diff --git a/src/reactpy_django/exceptions.py b/src/reactpy_django/exceptions.py index 412d647f..c0d4b32d 100644 --- a/src/reactpy_django/exceptions.py +++ b/src/reactpy_django/exceptions.py @@ -1,34 +1,25 @@ -class ComponentParamError(TypeError): - ... +class ComponentParamError(TypeError): ... -class ComponentDoesNotExistError(AttributeError): - ... +class ComponentDoesNotExistError(AttributeError): ... -class OfflineComponentMissing(ComponentDoesNotExistError): - ... +class OfflineComponentMissingError(ComponentDoesNotExistError): ... -class InvalidHostError(ValueError): - ... +class InvalidHostError(ValueError): ... -class ComponentCarrierError(Exception): - ... +class ComponentCarrierError(Exception): ... -class UserNotFoundError(Exception): - ... +class UserNotFoundError(Exception): ... -class ViewNotRegisteredError(AttributeError): - ... +class ViewNotRegisteredError(AttributeError): ... -class ViewDoesNotExistError(AttributeError): - ... +class ViewDoesNotExistError(AttributeError): ... -class DecoratorParamError(TypeError): - ... +class DecoratorParamError(TypeError): ... diff --git a/src/reactpy_django/forms/__init__.py b/src/reactpy_django/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/reactpy_django/forms/components.py b/src/reactpy_django/forms/components.py new file mode 100644 index 00000000..a10d5c92 --- /dev/null +++ b/src/reactpy_django/forms/components.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Union, cast +from uuid import uuid4 + +from django.forms import Form, ModelForm +from reactpy import component, hooks, html, utils +from reactpy.core.events import event +from reactpy.web import export, module_from_file + +from reactpy_django.forms.transforms import ( + convert_html_props_to_reactjs, + convert_textarea_children_to_prop, + infer_key_from_attributes, + intercept_anchor_links, + set_value_prop_on_select_element, + transform_value_prop_on_input_element, +) +from reactpy_django.forms.utils import convert_form_fields, validate_form_args +from reactpy_django.types import AsyncFormEvent, FormEventData, SyncFormEvent +from reactpy_django.utils import ensure_async + +if TYPE_CHECKING: + from collections.abc import Sequence + + from reactpy.core.types import VdomDict + +DjangoForm = export( + module_from_file("reactpy-django", file=Path(__file__).parent.parent / "static" / "reactpy_django" / "index.js"), + ("DjangoForm"), +) + + +@component +def _django_form( + form: type[Form | ModelForm], + on_success: AsyncFormEvent | SyncFormEvent | None, + on_error: AsyncFormEvent | SyncFormEvent | None, + on_receive_data: AsyncFormEvent | SyncFormEvent | None, + on_change: AsyncFormEvent | SyncFormEvent | None, + auto_save: bool, + extra_props: dict, + extra_transforms: Sequence[Callable[[VdomDict], Any]], + form_template: str | None, + thread_sensitive: bool, + top_children: Sequence, + bottom_children: Sequence, +): + from reactpy_django import config + + uuid = hooks.use_ref(uuid4().hex.replace("-", "")).current + top_children_count = hooks.use_ref(len(top_children)) + bottom_children_count = hooks.use_ref(len(bottom_children)) + submitted_data, set_submitted_data = hooks.use_state({} or None) + rendered_form, set_rendered_form = hooks.use_state(cast(Union[str, None], None)) + + # Initialize the form with the provided data + validate_form_args(top_children, top_children_count, bottom_children, bottom_children_count, form) + initialized_form = form(data=submitted_data) + form_event = FormEventData( + form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data + ) + + # Validate and render the form + @hooks.use_effect(dependencies=[str(submitted_data)]) + async def render_form(): + """Forms must be rendered in an async loop to allow database fields to execute.""" + if submitted_data: + await ensure_async(initialized_form.full_clean, thread_sensitive=thread_sensitive)() + success = not initialized_form.errors.as_data() + if success and on_success: + await ensure_async(on_success, thread_sensitive=thread_sensitive)(form_event) + if not success and on_error: + await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event) + if success and auto_save and isinstance(initialized_form, ModelForm): + await ensure_async(initialized_form.save)() + set_submitted_data(None) + + set_rendered_form( + await ensure_async(initialized_form.render)(form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE) + ) + + async def on_submit_callback(new_data: dict[str, Any]): + """Callback function provided directly to the client side listener. This is responsible for transmitting + the submitted form data to the server for processing.""" + convert_form_fields(new_data, initialized_form) + + if on_receive_data: + new_form_event = FormEventData( + form=initialized_form, submitted_data=new_data, set_submitted_data=set_submitted_data + ) + await ensure_async(on_receive_data, thread_sensitive=thread_sensitive)(new_form_event) + + if submitted_data != new_data: + set_submitted_data(new_data) + + async def _on_change(_event): + """Event that exist solely to allow the user to detect form changes.""" + if on_change: + await ensure_async(on_change, thread_sensitive=thread_sensitive)(form_event) + + if not rendered_form: + return None + + return html.form( + extra_props + | { + "id": f"reactpy-{uuid}", + # Intercept the form submission to prevent the browser from navigating + "onSubmit": event(lambda _: None, prevent_default=True), + "onChange": _on_change, + }, + DjangoForm({"onSubmitCallback": on_submit_callback, "formId": f"reactpy-{uuid}"}), + *top_children, + utils.html_to_vdom( + rendered_form, + convert_html_props_to_reactjs, + convert_textarea_children_to_prop, + set_value_prop_on_select_element, + transform_value_prop_on_input_element, + intercept_anchor_links, + infer_key_from_attributes, + *extra_transforms, + strict=False, + ), + *bottom_children, + ) diff --git a/src/reactpy_django/forms/transforms.py b/src/reactpy_django/forms/transforms.py new file mode 100644 index 00000000..1a757b77 --- /dev/null +++ b/src/reactpy_django/forms/transforms.py @@ -0,0 +1,488 @@ +# type: ignore +# TODO: Almost everything in this module should be moved to `reactpy.utils._mutate_vdom()`. +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from reactpy.core.events import EventHandler, to_event_handler_function + +if TYPE_CHECKING: + from reactpy.core.types import VdomDict + + +def convert_html_props_to_reactjs(vdom_tree: VdomDict) -> VdomDict: + """Transformation that standardizes the prop names to be used in the component.""" + # On each node, replace the 'attributes' key names with the standardized names. + if "attributes" in vdom_tree: + vdom_tree["attributes"] = {_normalize_prop_name(k): v for k, v in vdom_tree["attributes"].items()} + + return vdom_tree + + +def convert_textarea_children_to_prop(vdom_tree: VdomDict) -> VdomDict: + """Transformation that converts the text content of a