From 3fefc8d7449b128a6370acaab2f7bfb5760c6de3 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 17 Jul 2023 13:16:42 -0600 Subject: [PATCH 01/22] Fix link usages (#21) * Fix link usages. * remove run() --- docs/src/usage.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 0bf0387..e0f7bb2 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -92,16 +92,21 @@ This will allow ReactPy to handle the transition between routes more quickly by the cost of a full page load. ```python -from reactpy import component, html, run -from reactpy_router import link, route, simple, use_location +from reactpy import component, html, run, use_location +from reactpy_router import link, route, simple @component def root(): - location = use_location() + use_location() return simple.router( - route("/", html.h1("Home Page 🏠")), + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link(html.button("About"), to="/about"), + ), + ), route("/about", html.h1("About Page 📖")), - link("/about", html.button("About")), ) ``` @@ -127,10 +132,16 @@ from reactpy_router import link, route, simple, use_query @component def root(): + use_location() return simple.router( - route("/", html.h1("Home Page 🏠")), - route("/search", search()), - link("Search", to="/search?q=reactpy"), + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link("Search", to="/search?q=reactpy"), + ), + ), + route("/about", html.h1("About Page 📖")), ) @component @@ -152,9 +163,14 @@ from reactpy_router import link, route, simple, use_params @component def root(): return simple.router( - route("/", html.h1("Home Page 🏠")), + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link("User 123", to="/user/123"), + ), + ), route("/user/{id:int}", user()), - link("User 123", to="/user/123"), ) @component From 5336360dd353a58de8d329274f5ecece0ac6b209 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 17 Jul 2023 13:43:40 -0600 Subject: [PATCH 02/22] Publish docs via GH actions (#22) * Publish docs via GH actions * Update publish-docs.yaml --- .github/workflows/publish-docs.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/publish-docs.yaml diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml new file mode 100644 index 0000000..2d6aa80 --- /dev/null +++ b/.github/workflows/publish-docs.yaml @@ -0,0 +1,21 @@ +name: publish-docs + +on: + push: + branches: + - main + +jobs: + build: + name: Deploy docs + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@v2 + - name: Deploy docs + # Use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to exclude mkdocs-material theme + uses: mhausenblas/mkdocs-deploy-gh-pages@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CONFIG_FILE: docs/mkdocs.yml + REQUIREMENTS: requirements/build-docs.txt From 475e3578bceae4733ea48860d3ae2a7fe9b376eb Mon Sep 17 00:00:00 2001 From: Denis Povod Date: Thu, 14 Sep 2023 19:50:23 +0200 Subject: [PATCH 03/22] fix: fix relative navigation (#24) --- js/src/index.js | 2 +- tests/test_core.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/js/src/index.js b/js/src/index.js index 2c06733..1f43092 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -33,7 +33,7 @@ export function History({ onChange }) { export function Link({ to, onClick, children, ...props }) { const handleClick = (event) => { event.preventDefault(); - window.history.pushState({}, to, window.location.origin + to); + window.history.pushState({}, to, new URL(to, window.location)); onClick({ pathname: window.location.pathname, search: window.location.search, diff --git a/tests/test_core.py b/tests/test_core.py index 47ca951..5f05f5c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -183,3 +183,43 @@ def sample(): await display.page.go_back() await display.page.wait_for_selector("#root") + + +async def test_relative_links(display: DisplayFixture): + @component + def sample(): + return simple.router( + route("/", link("Root", to="/a", id="root")), + route("/a", link("A", to="/a/b", id="a")), + route("/a/b", link("B", to="../a/b/c", id="b")), + route("/a/b/c", link("C", to="../d", id="c")), + route("/a/d", link("D", to="e", id="d")), + route("/a/e", link("E", to="../default", id="e")), + route("*", html.h1({"id": "default"}, "Default")), + ) + + await display.show(sample) + + for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e"]: + lnk = await display.page.wait_for_selector(link_selector) + await lnk.click() + + await display.page.wait_for_selector("#default") + + await display.page.go_back() + await display.page.wait_for_selector("#e") + + await display.page.go_back() + await display.page.wait_for_selector("#d") + + await display.page.go_back() + await display.page.wait_for_selector("#c") + + await display.page.go_back() + await display.page.wait_for_selector("#b") + + await display.page.go_back() + await display.page.wait_for_selector("#a") + + await display.page.go_back() + await display.page.wait_for_selector("#root") From 3dd9777e6cad5347600ca4b6fc8d725c203dc3c4 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Wed, 13 Dec 2023 14:53:13 -0700 Subject: [PATCH 04/22] version 0.1.1 (#25) --- reactpy_router/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactpy_router/__init__.py b/reactpy_router/__init__.py index 0fa3ea1..cb2fcbc 100644 --- a/reactpy_router/__init__.py +++ b/reactpy_router/__init__.py @@ -1,5 +1,5 @@ # the version is statically loaded by setup.py -__version__ = "0.1.0" +__version__ = "0.1.1" from . import simple from .core import create_router, link, route, router_component, use_params, use_query From 0f4dea2b0cf3da1d783f62f6609d21c635b5c785 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Fri, 16 Feb 2024 20:54:51 -0800 Subject: [PATCH 05/22] Refactor the repo, and new docs styling (#29) --- .github/CODEOWNERS | 1 + .github/FUNDING.yml | 12 + .github/ISSUE_TEMPLATE/bug_report.md | 24 -- .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/doc_enhancement.md | 14 - .github/ISSUE_TEMPLATE/feature_request.md | 20 -- .github/ISSUE_TEMPLATE/issue-form.yml | 16 + .github/pull_request_template.md | 14 + .github/workflows/codeql.yml | 78 ++++ .github/workflows/publish-develop-docs.yml | 22 ++ .github/workflows/publish-docs.yaml | 21 -- .github/workflows/publish-py.yaml | 29 ++ .github/workflows/publish-release-docs.yml | 23 ++ .github/workflows/release.yaml | 30 -- .github/workflows/test-docs.yml | 37 ++ .github/workflows/test-src.yaml | 40 +++ .github/workflows/test.yaml | 39 -- .gitignore | 17 +- CHANGELOG.md | 68 ++++ CODE_OF_CONDUCT.md | 47 +++ LICENSE | 21 -- LICENSE.md | 9 + MANIFEST.in | 6 +- README.md | 24 +- docs/examples/python/__init__.py | 0 .../python/basic-routing-more-routes.py | 15 + docs/examples/python/basic-routing.py | 14 + docs/examples/python/nested-routes.py | 89 +++++ docs/examples/python/route-links.py | 23 ++ docs/examples/python/route-parameters.py | 89 +++++ docs/examples/python/use-params.py | 23 ++ docs/examples/python/use-query.py | 23 ++ docs/includes/pr.md | 3 + docs/mkdocs.yml | 184 +++++++--- .../add-interactivity-demo.html | 172 +++++++++ .../home-code-examples/add-interactivity.py | 30 ++ .../home-code-examples/code-block.html | 7 + .../create-user-interfaces-demo.html | 24 ++ .../create-user-interfaces.py | 22 ++ .../write-components-with-python-demo.html | 65 ++++ .../write-components-with-python.py | 15 + docs/overrides/main.html | 20 ++ docs/src/about/changelog.md | 14 + docs/src/about/code.md | 53 +++ docs/src/about/docs.md | 45 +++ docs/src/about/license.md | 8 + docs/src/assets/css/admonition.css | 160 +++++++++ docs/src/assets/css/banner.css | 15 + docs/src/assets/css/button.css | 41 +++ docs/src/assets/css/code.css | 111 ++++++ docs/src/assets/css/footer.css | 33 ++ docs/src/assets/css/home.css | 335 ++++++++++++++++++ docs/src/assets/css/main.css | 85 +++++ docs/src/assets/css/navbar.css | 185 ++++++++++ docs/src/assets/css/sidebar.css | 104 ++++++ docs/src/assets/css/table-of-contents.css | 48 +++ docs/src/assets/js/main.js | 19 + docs/src/assets/logo.svg | 160 --------- docs/src/contributing.md | 70 ---- docs/src/dictionary.txt | 39 ++ docs/src/index.md | 19 +- .../src/{tutorials => learn}/custom-router.md | 0 docs/src/learn/hooks.md | 27 ++ docs/src/learn/routers-routes-and-links.md | 67 ++++ docs/src/learn/simple-application.md | 88 +++++ docs/src/reference.md | 5 - docs/src/reference/core.md | 1 + docs/src/reference/router.md | 1 + docs/src/reference/types.md | 1 + docs/src/tutorials/simple-app.md | 277 --------------- docs/src/usage.md | 180 ---------- js/README.md | 22 -- noxfile.py | 82 ++--- pyproject.toml | 25 +- requirements.txt | 3 +- requirements/build-docs.txt | 12 +- requirements/build-pkg.txt | 3 + requirements/check-style.txt | 7 +- requirements/test-env.txt | 1 + requirements/{nox-deps.txt => test-run.txt} | 0 setup.cfg | 9 - setup.py | 99 +++--- {js => src/js}/.eslintrc.json | 0 {js => src/js}/package-lock.json | 0 {js => src/js}/package.json | 0 {js => src/js}/rollup.config.js | 0 {js => src/js}/src/index.js | 0 .../reactpy_router}/__init__.py | 0 .../reactpy_router}/core.py | 2 +- .../reactpy_router}/py.typed | 0 .../reactpy_router}/simple.py | 0 .../reactpy_router}/types.py | 0 tests/test_core.py | 9 +- 93 files changed, 2801 insertions(+), 1099 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/doc_enhancement.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/issue-form.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/publish-develop-docs.yml delete mode 100644 .github/workflows/publish-docs.yaml create mode 100644 .github/workflows/publish-py.yaml create mode 100644 .github/workflows/publish-release-docs.yml delete mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/test-docs.yml create mode 100644 .github/workflows/test-src.yaml delete mode 100644 .github/workflows/test.yaml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 docs/examples/python/__init__.py create mode 100644 docs/examples/python/basic-routing-more-routes.py create mode 100644 docs/examples/python/basic-routing.py create mode 100644 docs/examples/python/nested-routes.py create mode 100644 docs/examples/python/route-links.py create mode 100644 docs/examples/python/route-parameters.py create mode 100644 docs/examples/python/use-params.py create mode 100644 docs/examples/python/use-query.py create mode 100644 docs/includes/pr.md create mode 100644 docs/overrides/home-code-examples/add-interactivity-demo.html create mode 100644 docs/overrides/home-code-examples/add-interactivity.py create mode 100644 docs/overrides/home-code-examples/code-block.html create mode 100644 docs/overrides/home-code-examples/create-user-interfaces-demo.html create mode 100644 docs/overrides/home-code-examples/create-user-interfaces.py create mode 100644 docs/overrides/home-code-examples/write-components-with-python-demo.html create mode 100644 docs/overrides/home-code-examples/write-components-with-python.py create mode 100644 docs/overrides/main.html create mode 100644 docs/src/about/changelog.md create mode 100644 docs/src/about/code.md create mode 100644 docs/src/about/docs.md create mode 100644 docs/src/about/license.md create mode 100644 docs/src/assets/css/admonition.css create mode 100644 docs/src/assets/css/banner.css create mode 100644 docs/src/assets/css/button.css create mode 100644 docs/src/assets/css/code.css create mode 100644 docs/src/assets/css/footer.css create mode 100644 docs/src/assets/css/home.css create mode 100644 docs/src/assets/css/main.css create mode 100644 docs/src/assets/css/navbar.css create mode 100644 docs/src/assets/css/sidebar.css create mode 100644 docs/src/assets/css/table-of-contents.css create mode 100644 docs/src/assets/js/main.js delete mode 100644 docs/src/assets/logo.svg delete mode 100644 docs/src/contributing.md create mode 100644 docs/src/dictionary.txt rename docs/src/{tutorials => learn}/custom-router.md (100%) create mode 100644 docs/src/learn/hooks.md create mode 100644 docs/src/learn/routers-routes-and-links.md create mode 100644 docs/src/learn/simple-application.md delete mode 100644 docs/src/reference.md create mode 100644 docs/src/reference/core.md create mode 100644 docs/src/reference/router.md create mode 100644 docs/src/reference/types.md delete mode 100644 docs/src/tutorials/simple-app.md delete mode 100644 docs/src/usage.md delete mode 100644 js/README.md create mode 100644 requirements/build-pkg.txt rename requirements/{nox-deps.txt => test-run.txt} (100%) rename {js => src/js}/.eslintrc.json (100%) rename {js => src/js}/package-lock.json (100%) rename {js => src/js}/package.json (100%) rename {js => src/js}/rollup.config.js (100%) rename {js => src/js}/src/index.js (100%) rename {reactpy_router => src/reactpy_router}/__init__.py (100%) rename {reactpy_router => src/reactpy_router}/core.py (98%) rename {reactpy_router => src/reactpy_router}/py.typed (100%) rename {reactpy_router => src/reactpy_router}/simple.py (100%) rename {reactpy_router => src/reactpy_router}/types.py (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1ff35c8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @reactive-python/maintainers diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..12f72a6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [archmonger] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c6bdd8f..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: Bug Report -labels: bug -assignees: rmorshea - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..36e5aeb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Start a Discussion + url: https://github.com/reactive-python/reactpy-router/discussions + about: Report issues, request features, ask questions, and share ideas diff --git a/.github/ISSUE_TEMPLATE/doc_enhancement.md b/.github/ISSUE_TEMPLATE/doc_enhancement.md deleted file mode 100644 index 9a960b0..0000000 --- a/.github/ISSUE_TEMPLATE/doc_enhancement.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Doc enhancement -about: Documentation needs to be fixed or added -title: Doc Enhancement -labels: docs -assignees: rmorshea - ---- - -**Describe what documentation needs to be fixed or added** -Is something missing, worded poorly, or flat out wrong? Tells us about it here. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1c5de5f..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: rmorshea - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/issue-form.yml b/.github/ISSUE_TEMPLATE/issue-form.yml new file mode 100644 index 0000000..b4a4b89 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-form.yml @@ -0,0 +1,16 @@ +name: Plan a Task +description: Create a detailed plan of action (ONLY START AFTER DISCUSSION PLEASE 🙏). +labels: ["flag: triage"] +body: +- type: textarea + attributes: + label: Current Situation + description: Discuss how things currently are, why they require action, and any relevant prior discussion/context. + validations: + required: false +- type: textarea + attributes: + label: Proposed Actions + description: Describe what ought to be done, and why that will address the reasons for action mentioned above. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a555320 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## Description + + + +## Checklist + +Please update this checklist as you complete each item: + +- [ ] Tests have been developed for bug fixes or new functionality. +- [ ] The changelog has been updated, if necessary. +- [ ] Documentation has been updated, if necessary. +- [ ] GitHub Issues closed by this PR have been linked. + +By submitting this pull request I agree that all contributions comply with this project's open source license(s). diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..0f26793 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + # Runs at 22:21 on Monday. + - cron: '21 22 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # 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 + + # â„šī¸ 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 + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml new file mode 100644 index 0000000..6b1d4de --- /dev/null +++ b/.github/workflows/publish-develop-docs.yml @@ -0,0 +1,22 @@ +name: Publish Develop Docs +on: + push: + branches: + - main +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - run: pip install -r requirements/build-docs.txt + - name: Publish Develop Docs + run: | + git config user.name github-actions + git config user.email github-actions@github.com + cd docs + mike deploy --push develop diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml deleted file mode 100644 index 2d6aa80..0000000 --- a/.github/workflows/publish-docs.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: publish-docs - -on: - push: - branches: - - main - -jobs: - build: - name: Deploy docs - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v2 - - name: Deploy docs - # Use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to exclude mkdocs-material theme - uses: mhausenblas/mkdocs-deploy-gh-pages@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CONFIG_FILE: docs/mkdocs.yml - REQUIREMENTS: requirements/build-docs.txt diff --git a/.github/workflows/publish-py.yaml b/.github/workflows/publish-py.yaml new file mode 100644 index 0000000..34ae5fa --- /dev/null +++ b/.github/workflows/publish-py.yaml @@ -0,0 +1,29 @@ +# 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: + publish-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + 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 setup.py bdist_wheel + twine upload dist/* diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml new file mode 100644 index 0000000..6fc3233 --- /dev/null +++ b/.github/workflows/publish-release-docs.yml @@ -0,0 +1,23 @@ +name: Publish Release Docs + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - run: pip install -r requirements/build-docs.txt + - name: Publish ${{ github.event.release.name }} Docs + 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 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 00a3264..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,30 +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: release - -on: - release: - types: - - created - -jobs: - publish-package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: "3.x" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py bdist_wheel - twine upload dist/* diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml new file mode 100644 index 0000000..d5f5052 --- /dev/null +++ b/.github/workflows/test-docs.yml @@ -0,0 +1,37 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - 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 + - 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/ + black docs/examples/python/ --check + ruff check docs/examples/python/ diff --git a/.github/workflows/test-src.yaml b/.github/workflows/test-src.yaml new file mode 100644 index 0000000..b5ae7d0 --- /dev/null +++ b/.github/workflows/test-src.yaml @@ -0,0 +1,40 @@ +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"] + steps: + - uses: actions/checkout@v3 + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + 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 + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Latest Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + - name: Install Python Dependencies + run: pip install -r requirements/test-run.txt + - name: Run Tests + run: nox -t test -- --coverage diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 7e0a9c8..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: test - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Use Latest Python - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - name: Install Python Dependencies - run: pip install -r requirements/nox-deps.txt - - name: Run Tests - run: nox -t test - - environments: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v2 - - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Python Dependencies - run: pip install -r requirements/nox-deps.txt - - name: Run Tests - run: nox -t test -- --no-cov diff --git a/.gitignore b/.gitignore index c9676bc..9155bda 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ docs/site # --- JAVASCRIPT BUNDLES --- -reactpy_router/bundle.js +src/reactpy_router/bundle.js # --- PYTHON IGNORE FILES ---- @@ -65,21 +65,6 @@ cover/ *.mo *.pot -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ # PyBuilder .pybuilder/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c673a2f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +All notable changes to this project will be documented in this file. + + + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + + + + + +## [Unreleased] + +- Nothing (yet)! + +## [0.1.1] - 2023-12-13 + +### Fixed + +- Fixed relative navigation. + +## [0.1.0] - 2023-06-16 + +### Added + +- Automatically handle client-side history changes. + +## [0.0.1] - 2023-05-10 + +### Added + +- Add robust lint/testing. +- Upgrade `reactpy`. +- More robust routing with `starlette`. +- Initial draft of router compiler. + +### Changed + +- Rename `configure` to `create_router`. +- Rename from `idom-router` to `reactpy-router`. + +[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...HEAD +[0.1.1]: https://github.com/reactive-python/reactpy-router/compare/0.1.0...0.1.1 +[0.1.0]: https://github.com/reactive-python/reactpy-router/compare/0.0.1...0.1.0 +[0.0.1]: https://github.com/reactive-python/reactpy-router/releases/tag/0.0.1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..809177a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,47 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ryan.morshead@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1067742..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2022 Ryan S. Morshead - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f5423c3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +## The MIT License (MIT) + +#### Copyright (c) Reactive Python and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 9a3edbc..bdca1f4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,2 @@ -include README.md -include reactpy_router/bundle.js -include reactpy_router/py.typed -include LICENSE +include src/reactpy_router/bundle.js +include src/reactpy_router/py.typed diff --git a/README.md b/README.md index 63aeaab..3cb0c5d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ -# reactpy-router +# ReactPy Router -A URL router for ReactPy +

+ + + + + + + + + + + + + + + +

-Read the docs: https://reactive-python.github.io/reactpy-router +[ReactPy-Router](https://github.com/reactive-python/reactpy-router) is used to add used to add URL routing support to an existing **ReactPy project**. + +More information about this package can be found on [the documentation](https://reactive-python.github.io/reactpy-router). diff --git a/docs/examples/python/__init__.py b/docs/examples/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/examples/python/basic-routing-more-routes.py b/docs/examples/python/basic-routing-more-routes.py new file mode 100644 index 0000000..8ddbebb --- /dev/null +++ b/docs/examples/python/basic-routing-more-routes.py @@ -0,0 +1,15 @@ +from reactpy import component, html, run + +from reactpy_router import route, simple + + +@component +def root(): + return simple.router( + route("/", html.h1("Home Page 🏠")), + route("/messages", html.h1("Messages đŸ’Ŧ")), + route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + ) + + +run(root) diff --git a/docs/examples/python/basic-routing.py b/docs/examples/python/basic-routing.py new file mode 100644 index 0000000..57b7a37 --- /dev/null +++ b/docs/examples/python/basic-routing.py @@ -0,0 +1,14 @@ +from reactpy import component, html, run + +from reactpy_router import route, simple + + +@component +def root(): + return simple.router( + route("/", html.h1("Home Page 🏠")), + route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + ) + + +run(root) diff --git a/docs/examples/python/nested-routes.py b/docs/examples/python/nested-routes.py new file mode 100644 index 0000000..f03a692 --- /dev/null +++ b/docs/examples/python/nested-routes.py @@ -0,0 +1,89 @@ +from typing import TypedDict + +from reactpy import component, html, run +from reactpy_router import link, route, simple + +message_data: list["MessageDataType"] = [ + {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, + {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, + {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, + {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, + {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, + {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, + {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, + {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, +] + + +@component +def root(): + return simple.router( + route("/", home()), + route( + "/messages", + all_messages(), + # we'll improve upon these manually created routes in the next section... + route("/with/Alice", messages_with("Alice")), + route("/with/Alice-Bob", messages_with("Alice", "Bob")), + ), + route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + ) + + +@component +def home(): + return html.div( + html.h1("Home Page 🏠"), + link("Messages", to="/messages"), + ) + + +@component +def all_messages(): + last_messages = { + ", ".join(msg["with"]): msg + for msg in sorted(message_data, key=lambda m: m["id"]) + } + return html.div( + html.h1("All Messages đŸ’Ŧ"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + html.p( + link( + f"Conversation with: {', '.join(msg['with'])}", + to=f"/messages/with/{'-'.join(msg['with'])}", + ), + ), + f"{'' if msg['from'] is None else '🔴'} {msg['message']}", + ) + for msg in last_messages.values() + ] + ), + ) + + +@component +def messages_with(*names): + messages = [msg for msg in message_data if set(msg["with"]) == names] + return html.div( + html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + f"{msg['from'] or 'You'}: {msg['message']}", + ) + for msg in messages + ] + ), + ) + + +run(root) + +MessageDataType = TypedDict( + "MessageDataType", + {"id": int, "with": list[str], "from": str | None, "message": str}, +) diff --git a/docs/examples/python/route-links.py b/docs/examples/python/route-links.py new file mode 100644 index 0000000..f2be305 --- /dev/null +++ b/docs/examples/python/route-links.py @@ -0,0 +1,23 @@ +from reactpy import component, html, run + +from reactpy_router import link, route, simple + + +@component +def root(): + return simple.router( + route("/", home()), + route("/messages", html.h1("Messages đŸ’Ŧ")), + route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + ) + + +@component +def home(): + return html.div( + html.h1("Home Page 🏠"), + link("Messages", to="/messages"), + ) + + +run(root) diff --git a/docs/examples/python/route-parameters.py b/docs/examples/python/route-parameters.py new file mode 100644 index 0000000..4fd30e2 --- /dev/null +++ b/docs/examples/python/route-parameters.py @@ -0,0 +1,89 @@ +from typing import TypedDict + +from reactpy import component, html, run +from reactpy_router import link, route, simple +from reactpy_router.core import use_params + +message_data: list["MessageDataType"] = [ + {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, + {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, + {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, + {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, + {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, + {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, + {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, + {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, +] + + +@component +def root(): + return simple.router( + route("/", home()), + route( + "/messages", + all_messages(), + route("/with/{names}", messages_with()), # note the path param + ), + route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + ) + + +@component +def home(): + return html.div( + html.h1("Home Page 🏠"), + link("Messages", to="/messages"), + ) + + +@component +def all_messages(): + last_messages = { + ", ".join(msg["with"]): msg + for msg in sorted(message_data, key=lambda m: m["id"]) + } + return html.div( + html.h1("All Messages đŸ’Ŧ"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + html.p( + link( + f"Conversation with: {', '.join(msg['with'])}", + to=f"/messages/with/{'-'.join(msg['with'])}", + ), + ), + f"{'' if msg['from'] is None else '🔴'} {msg['message']}", + ) + for msg in last_messages.values() + ] + ), + ) + + +@component +def messages_with(): + names = set(use_params()["names"].split("-")) # and here we use the path param + messages = [msg for msg in message_data if set(msg["with"]) == names] + return html.div( + html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + f"{msg['from'] or 'You'}: {msg['message']}", + ) + for msg in messages + ] + ), + ) + + +run(root) + +MessageDataType = TypedDict( + "MessageDataType", + {"id": int, "with": list[str], "from": str | None, "message": str}, +) diff --git a/docs/examples/python/use-params.py b/docs/examples/python/use-params.py new file mode 100644 index 0000000..7b1193a --- /dev/null +++ b/docs/examples/python/use-params.py @@ -0,0 +1,23 @@ +from reactpy import component, html + +from reactpy_router import link, route, simple, use_params + + +@component +def user(): + params = use_params() + return html.h1(f"User {params['id']} 👤") + + +@component +def root(): + return simple.router( + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link("User 123", to="/user/123"), + ), + ), + route("/user/{id:int}", user()), + ) diff --git a/docs/examples/python/use-query.py b/docs/examples/python/use-query.py new file mode 100644 index 0000000..a8678cc --- /dev/null +++ b/docs/examples/python/use-query.py @@ -0,0 +1,23 @@ +from reactpy import component, html + +from reactpy_router import link, route, simple, use_query + + +@component +def search(): + query = use_query() + return html.h1(f"Search Results for {query['q'][0]} 🔍") + + +@component +def root(): + return simple.router( + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link("Search", to="/search?q=reactpy"), + ), + ), + route("/about", html.h1("About Page 📖")), + ) diff --git a/docs/includes/pr.md b/docs/includes/pr.md new file mode 100644 index 0000000..9b4f0e4 --- /dev/null +++ b/docs/includes/pr.md @@ -0,0 +1,3 @@ +Now, you can create/modify the ReactPy-Router source code, and Pull Request (PR) your changes to our GitHub repository. + +To learn how to create GitHub PRs, [click here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 54a4f8c..d93b302 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,55 +1,149 @@ -site_name: ReactPy Router -docs_dir: src -repo_url: https://github.com/reactive-python/reactpy-router - +--- nav: - - Home: index.md - - Usage: usage.md - - Tutorials: - - Simple Application: tutorials/simple-app.md - - Custom Router: tutorials/custom-router.md - - Reference: reference.md - - Contributing: contributing.md - - Source Code: https://github.com/reactive-python/reactpy-router + - Get Started: + - Add ReactPy-Router to Your Project: index.md + - Your First Routed Application: learn/simple-application.md + - Advanced Topics: + - Routers, Routes, and Links: learn/routers-routes-and-links.md + - Hooks: learn/hooks.md + - Creating a Custom Router 🚧: learn/custom-router.md + - Reference: + - Core: reference/core.md + - Router: reference/router.md + - Types: reference/types.md + - About: + - Changelog: about/changelog.md + - Contributor Guide: + - Code: about/code.md + - Docs: about/docs.md + - Community: + - GitHub Discussions: https://github.com/reactive-python/reactpy-router/discussions + - Discord: https://discord.gg/uNb5P4hA9X + - Reddit: https://www.reddit.com/r/ReactPy/ + - License: about/license.md theme: - name: material - logo: assets/logo.svg - favicon: assets/logo.svg - palette: - # Palette toggle for light mode - - scheme: default - toggle: - icon: material/brightness-7 - name: Switch to dark mode - primary: black - accent: light-blue - - # Palette toggle for dark mode - - scheme: slate - toggle: - icon: material/brightness-4 - name: Switch to light mode - primary: black - accent: light-blue + name: material + custom_dir: overrides + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/white-balance-sunny + name: Switch to light mode + primary: red # We use red to indicate that something is unthemed + accent: red + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-night + name: Switch to dark mode + primary: white + accent: red + features: + - navigation.instant + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - content.code.copy + - search.highlight + icon: + repo: fontawesome/brands/github + admonition: + note: fontawesome/solid/note-sticky + logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg + favicon: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg +markdown_extensions: + - toc: + permalink: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + linenums: true + - pymdownx.superfences + - pymdownx.details + - pymdownx.inlinehilite + - admonition + - attr_list + - md_in_html + - pymdownx.keys plugins: -- search -- mkdocstrings: - default_handler: python - handlers: - python: - paths: ["../"] - import: - - https://reactpy.dev/docs/objects.inv - - https://installer.readthedocs.io/en/stable/objects.inv + - search + - include-markdown + - git-authors + - minify: + minify_html: true + minify_js: true + minify_css: true + cache_safe: true + - git-revision-date-localized: + fallback_to_build_date: true + - spellcheck: + known_words: dictionary.txt + allow_unicode: no + ignore_code: yes + skip_files: + - "index.md" + - "reference\\core.md" + - "reference/core.md" + - "reference\\types.md" + - "reference/types.md" + - mkdocstrings: + default_handler: python + handlers: + python: + paths: ["../"] + import: + - https://reactpy.dev/docs/objects.inv + - https://installer.readthedocs.io/en/stable/objects.inv -markdown_extensions: - - admonition - - pymdownx.details - - pymdownx.superfences +extra: + generator: false + version: + provider: mike + analytics: + provider: google + property: G-XRLQYZBG00 + +extra_javascript: + - assets/js/main.js + +extra_css: + - assets/css/main.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/banner.css + - assets/css/sidebar.css + - assets/css/navbar.css + - assets/css/table-of-contents.css + - assets/css/code.css + - assets/css/footer.css + - assets/css/home.css watch: - - "../reactpy_router" + - "../docs" + - ../README.md + - ../CHANGELOG.md + - ../LICENSE.md + - "../src" +site_name: ReactPy Router +site_author: Archmonger +site_description: It's React-Router, but in Python. +copyright: '© +
+ +Reactive Python and affiliates. +' +repo_url: https://github.com/reactive-python/reactpy-router +site_url: https://reactive-python.github.io/reactpy-router +repo_name: ReactPy Router (GitHub) +edit_uri: edit/main/docs/src/ +docs_dir: src diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/home-code-examples/add-interactivity-demo.html new file mode 100644 index 0000000..48ac19a --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity-demo.html @@ -0,0 +1,172 @@ +
+
+ +
+
+ + + + example.com/videos.html +
+
+ +
+
+

Searchable Videos

+

Type a search query below.

+ +
+ +

5 Videos

+ +
+
+ + + +
+
+

ReactPy: The Documentary

+

From web library to taco delivery service

+
+ +
+ +
+
+ + + +
+
+

Code using Worst Practices

+

Harriet Potter (2013)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Foriegn

+

Tim Cooker (2015)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Cooks

+

Soap Boat and Dinosaur Dan (2018)

+
+ +
+ +
+
+ + + +
+
+

Introducing Quantum Components

+

Isaac Asimov and Lauren-kun (2020)

+
+ +
+

+
+ + +
+
diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/home-code-examples/add-interactivity.py new file mode 100644 index 0000000..9097644 --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity.py @@ -0,0 +1,30 @@ +from reactpy import component, html, use_state + + +def filter_videos(videos, search_text): + return None + + +def search_input(dictionary, value): + return None + + +def video_list(videos, empty_heading): + return None + + +@component +def searchable_video_list(videos): + search_text, set_search_text = use_state("") + found_videos = filter_videos(videos, search_text) + + return html._( + search_input( + {"on_change": lambda new_text: set_search_text(new_text)}, + value=search_text, + ), + video_list( + videos=found_videos, + empty_heading=f"No matches for “{search_text}”", + ), + ) diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/home-code-examples/code-block.html new file mode 100644 index 0000000..c1f14e5 --- /dev/null +++ b/docs/overrides/home-code-examples/code-block.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+
diff --git a/docs/overrides/home-code-examples/create-user-interfaces-demo.html b/docs/overrides/home-code-examples/create-user-interfaces-demo.html new file mode 100644 index 0000000..9a684d3 --- /dev/null +++ b/docs/overrides/home-code-examples/create-user-interfaces-demo.html @@ -0,0 +1,24 @@ +
+
+
+
+ + + +
+
+

My video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/home-code-examples/create-user-interfaces.py b/docs/overrides/home-code-examples/create-user-interfaces.py new file mode 100644 index 0000000..37776ab --- /dev/null +++ b/docs/overrides/home-code-examples/create-user-interfaces.py @@ -0,0 +1,22 @@ +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-code-examples/write-components-with-python-demo.html b/docs/overrides/home-code-examples/write-components-with-python-demo.html new file mode 100644 index 0000000..203287c --- /dev/null +++ b/docs/overrides/home-code-examples/write-components-with-python-demo.html @@ -0,0 +1,65 @@ +
+
+

3 Videos

+
+
+ + + +
+
+

First video

+

Video description

+
+ +
+
+
+ + + +
+
+

Second video

+

Video description

+
+ +
+
+
+ + + +
+
+

Third video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/home-code-examples/write-components-with-python.py new file mode 100644 index 0000000..6af43ba --- /dev/null +++ b/docs/overrides/home-code-examples/write-components-with-python.py @@ -0,0 +1,15 @@ +from reactpy import component, html + + +@component +def video_list(videos, empty_heading): + count = len(videos) + heading = empty_heading + if count > 0: + noun = "Videos" if count > 1 else "Video" + heading = f"{count} {noun}" + + return html.section( + html.h2(heading), + [video(video) for video in videos], + ) diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000..c63ca9e --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +{{ super() }} + +{% if git_page_authors %} +
+ + Authors: {{ git_page_authors | default('enable mkdocs-git-authors-plugin') }} + +
+{% endif %} +{% endblock %} + +{% block outdated %} +You're not viewing the latest release. + + Click here to go to latest. + +{% endblock %} diff --git a/docs/src/about/changelog.md b/docs/src/about/changelog.md new file mode 100644 index 0000000..1ecf88e --- /dev/null +++ b/docs/src/about/changelog.md @@ -0,0 +1,14 @@ +--- +hide: + - toc +--- + +

+ +{% include-markdown "../../../CHANGELOG.md" start="" end="" %} + +

+ +--- + +{% include-markdown "../../../CHANGELOG.md" start="" %} diff --git a/docs/src/about/code.md b/docs/src/about/code.md new file mode 100644 index 0000000..5d73042 --- /dev/null +++ b/docs/src/about/code.md @@ -0,0 +1,53 @@ +## Overview + +

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

+ +--- + +## 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-router.git +cd reactpy-router +``` + +Then, by running the command below you can install the dependencies needed to run the ReactPy-Router development environment. + +```bash linenums="0" +pip install -r requirements.txt --upgrade --verbose +``` + +## 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 foreground with a visible browser window, run: + + + +```bash linenums="0" +nox -t test -- --headed +``` + +## Creating a pull request + +{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/about/docs.md b/docs/src/about/docs.md new file mode 100644 index 0000000..4c1f566 --- /dev/null +++ b/docs/src/about/docs.md @@ -0,0 +1,45 @@ +## 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-router.git +cd reactpy-router +``` + +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. + +## Creating a pull request + +{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/about/license.md b/docs/src/about/license.md new file mode 100644 index 0000000..15d975d --- /dev/null +++ b/docs/src/about/license.md @@ -0,0 +1,8 @@ +--- +hide: + - toc +--- + +--- + +{% include "../../../LICENSE.md" %} diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css new file mode 100644 index 0000000..8b3f06e --- /dev/null +++ b/docs/src/assets/css/admonition.css @@ -0,0 +1,160 @@ +[data-md-color-scheme="slate"] { + --admonition-border-color: transparent; + --admonition-expanded-border-color: rgba(255, 255, 255, 0.1); + --note-bg-color: rgba(43, 110, 98, 0.2); + --terminal-bg-color: #0c0c0c; + --terminal-title-bg-color: #000; + --deep-dive-bg-color: rgba(43, 52, 145, 0.2); + --you-will-learn-bg-color: #353a45; + --pitfall-bg-color: rgba(182, 87, 0, 0.2); +} +[data-md-color-scheme="default"] { + --admonition-border-color: rgba(0, 0, 0, 0.08); + --admonition-expanded-border-color: var(--admonition-border-color); + --note-bg-color: rgb(244, 251, 249); + --terminal-bg-color: rgb(64, 71, 86); + --terminal-title-bg-color: rgb(35, 39, 47); + --deep-dive-bg-color: rgb(243, 244, 253); + --you-will-learn-bg-color: rgb(246, 247, 249); + --pitfall-bg-color: rgb(254, 245, 231); +} + +.md-typeset details, +.md-typeset .admonition { + border-color: var(--admonition-border-color) !important; + box-shadow: none; +} + +.md-typeset :is(.admonition, details) { + margin: 0.55em 0; +} + +.md-typeset .admonition { + font-size: 0.7rem; +} + +.md-typeset .admonition:focus-within, +.md-typeset details:focus-within { + box-shadow: none !important; +} + +.md-typeset details[open] { + border-color: var(--admonition-expanded-border-color) !important; +} + +/* +Admonition: "summary" +React Name: "You will learn" +*/ +.md-typeset .admonition.summary { + background: var(--you-will-learn-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .summary .admonition-title { + font-size: 1rem; + background: transparent; + padding-left: 0.6rem; + padding-bottom: 0; +} + +.md-typeset .summary .admonition-title:before { + display: none; +} + +.md-typeset .admonition.summary { + border-color: #ffffff17 !important; +} + +/* +Admonition: "abstract" +React Name: "Note" +*/ +.md-typeset .admonition.abstract { + background: var(--note-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .abstract .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(68, 172, 153); +} + +.md-typeset .abstract .admonition-title:before { + font-size: 1.1rem; + background: rgb(68, 172, 153); +} + +/* +Admonition: "warning" +React Name: "Pitfall" +*/ +.md-typeset .admonition.warning { + background: var(--pitfall-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .warning .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(219, 125, 39); +} + +.md-typeset .warning .admonition-title:before { + font-size: 1.1rem; + background: rgb(219, 125, 39); +} + +/* +Admonition: "info" +React Name: "Deep Dive" +*/ +.md-typeset .admonition.info { + background: var(--deep-dive-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .info .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(136, 145, 236); +} + +.md-typeset .info .admonition-title:before { + font-size: 1.1rem; + background: rgb(136, 145, 236); +} + +/* +Admonition: "example" +React Name: "Terminal" +*/ +.md-typeset .admonition.example { + background: var(--terminal-bg-color); + border-radius: 0.4rem; + overflow: hidden; + border: none; +} + +.md-typeset .example .admonition-title { + background: var(--terminal-title-bg-color); + color: rgb(246, 247, 249); +} + +.md-typeset .example .admonition-title:before { + background: rgb(246, 247, 249); +} + +.md-typeset .admonition.example code { + background: transparent; + color: #fff; + box-shadow: none; +} diff --git a/docs/src/assets/css/banner.css b/docs/src/assets/css/banner.css new file mode 100644 index 0000000..3739a73 --- /dev/null +++ b/docs/src/assets/css/banner.css @@ -0,0 +1,15 @@ +body[data-md-color-scheme="slate"] { + --md-banner-bg-color: rgb(55, 81, 78); + --md-banner-font-color: #fff; +} + +body[data-md-color-scheme="default"] { + --md-banner-bg-color: #ff9; + --md-banner-font-color: #000; +} + +.md-banner--warning { + background-color: var(--md-banner-bg-color); + color: var(--md-banner-font-color); + text-align: center; +} diff --git a/docs/src/assets/css/button.css b/docs/src/assets/css/button.css new file mode 100644 index 0000000..8f71391 --- /dev/null +++ b/docs/src/assets/css/button.css @@ -0,0 +1,41 @@ +[data-md-color-scheme="slate"] { + --md-button-font-color: #fff; + --md-button-border-color: #404756; +} + +[data-md-color-scheme="default"] { + --md-button-font-color: #000; + --md-button-border-color: #8d8d8d; +} + +.md-typeset .md-button { + border-width: 1px; + border-color: var(--md-button-border-color); + border-radius: 9999px; + color: var(--md-button-font-color); + transition: color 125ms, background 125ms, border-color 125ms, + transform 125ms; +} + +.md-typeset .md-button:focus, +.md-typeset .md-button:hover { + border-color: var(--md-button-border-color); + color: var(--md-button-font-color); + background: rgba(78, 87, 105, 0.05); +} + +.md-typeset .md-button.md-button--primary { + color: #fff; + border-color: transparent; + background: var(--reactpy-color-dark); +} + +.md-typeset .md-button.md-button--primary:focus, +.md-typeset .md-button.md-button--primary:hover { + border-color: transparent; + background: var(--reactpy-color-darker); +} + +.md-typeset .md-button:focus { + transform: scale(0.98); +} diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css new file mode 100644 index 0000000..c546549 --- /dev/null +++ b/docs/src/assets/css/code.css @@ -0,0 +1,111 @@ +:root { + --code-max-height: 17.25rem; + --md-code-backdrop: rgba(0, 0, 0, 0) 0px 0px 0px 0px, + rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.03) 0px 0.8px 2px 0px, + rgba(0, 0, 0, 0.047) 0px 2.7px 6.7px 0px, + rgba(0, 0, 0, 0.08) 0px 12px 30px 0px; +} +[data-md-color-scheme="slate"] { + --md-code-hl-color: #ffffcf1c; + --md-code-bg-color: #16181d; + --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); + --code-tab-color: rgb(52, 58, 70); + --md-code-hl-name-color: #aadafc; + --md-code-hl-string-color: hsl(21 49% 63% / 1); + --md-code-hl-keyword-color: hsl(289.67deg 35% 60%); + --md-code-hl-constant-color: hsl(213.91deg 68% 61%); + --md-code-hl-number-color: #bfd9ab; + --func-and-decorator-color: #dcdcae; + --module-import-color: #60c4ac; +} +[data-md-color-scheme="default"] { + --md-code-hl-color: #ffffcf1c; + --md-code-bg-color: rgba(208, 211, 220, 0.4); + --md-code-fg-color: rgb(64, 71, 86); + --code-tab-color: #fff; + --func-and-decorator-color: var(--md-code-hl-function-color); + --module-import-color: #e153e5; +} +[data-md-color-scheme="default"] .md-typeset .highlight > pre > code, +[data-md-color-scheme="default"] .md-typeset .highlight > table.highlighttable { + --md-code-bg-color: #fff; +} + +/* All code blocks */ +.md-typeset pre > code { + max-height: var(--code-max-height); +} + +/* Code blocks with no line number */ +.md-typeset .highlight > pre > code { + border-radius: 16px; + max-height: var(--code-max-height); + box-shadow: var(--md-code-backdrop); +} + +/* Code blocks with line numbers */ +.md-typeset .highlighttable .linenos { + max-height: var(--code-max-height); + overflow: hidden; +} +.md-typeset .highlighttable { + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; +} + +/* Tabbed code blocks */ +.md-typeset .tabbed-set { + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--md-default-fg-color--lightest); +} +.md-typeset .tabbed-set .tabbed-block { + overflow: hidden; +} +.js .md-typeset .tabbed-set .tabbed-labels { + background: var(--code-tab-color); + margin: 0; + padding-left: 0.8rem; +} +.md-typeset .tabbed-set .tabbed-labels > label { + font-weight: 400; + font-size: 0.7rem; + padding-top: 0.55em; + padding-bottom: 0.35em; +} +.md-typeset .tabbed-set .highlighttable { + border-radius: 0; +} + +/* Code hightlighting colors */ + +/* Module imports */ +.highlight .nc, +.highlight .ne, +.highlight .nn, +.highlight .nv { + color: var(--module-import-color); +} + +/* Function def name and decorator */ +.highlight .nd, +.highlight .nf { + color: var(--func-and-decorator-color); +} + +/* None type */ +.highlight .kc { + color: var(--md-code-hl-constant-color); +} + +/* Keywords such as def and return */ +.highlight .k { + color: var(--md-code-hl-constant-color); +} + +/* HTML tags */ +.highlight .nt { + color: var(--md-code-hl-constant-color); +} diff --git a/docs/src/assets/css/footer.css b/docs/src/assets/css/footer.css new file mode 100644 index 0000000..b340828 --- /dev/null +++ b/docs/src/assets/css/footer.css @@ -0,0 +1,33 @@ +[data-md-color-scheme="slate"] { + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); + --md-footer-border-color: var(--md-header-border-color); +} + +[data-md-color-scheme="default"] { + --md-footer-fg-color: var(--md-typeset-color); + --md-footer-fg-color--light: var(--md-typeset-color); + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); + --md-footer-border-color: var(--md-header-border-color); +} + +.md-footer { + border-top: 1px solid var(--md-footer-border-color); +} + +.md-copyright { + width: 100%; +} + +.md-copyright__highlight { + width: 100%; +} + +.legal-footer-right { + float: right; +} + +.md-copyright__highlight div { + display: inline; +} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css new file mode 100644 index 0000000..c72e709 --- /dev/null +++ b/docs/src/assets/css/home.css @@ -0,0 +1,335 @@ +img.home-logo { + height: 120px; +} + +.home .row { + 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); +} + +.home .row.stripe { + 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; +} + +.home .row h1 { + 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; +} + +.home .row > p { + max-width: 35rem; + line-height: 1.5; + font-weight: 400; +} + +.home .row.first > p { + font-size: 32px; + font-weight: 500; +} + +/* Code blocks */ +.home .row .tabbed-set { + background: var(--home-tabbed-set-bg-color); + margin: 0; +} + +.home .row .tabbed-content { + 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; +} + +.home .row .tabbed-content { + -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; +} + +.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; +} + +.home .demo .vid-row { + display: flex; + flex-direction: row; + -moz-column-gap: 12px; + column-gap: 12px; +} + +.home .demo { + 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; +} + +.home .demo .vid-text { + 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; +} + +.home .demo h3 { + font-size: 16px; + line-height: 1.25; + margin: 0; +} + +.home .demo p { + 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; +} + +.home .demo .browser-navbar { + 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; +} + +.home .demo .browser-viewport .search-header > h1 { + 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; +} + +.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; +} + +.home .demo .search-bar svg { + height: 40px; + position: absolute; + transform: translateX(75%); +} + +.home .demo .search-bar { + 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; + } +} + +/* 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%; + } +} diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css new file mode 100644 index 0000000..6eefdf2 --- /dev/null +++ b/docs/src/assets/css/main.css @@ -0,0 +1,85 @@ +/* Variable overrides */ +:root { + --reactpy-color: #58b962; + --reactpy-color-dark: #42914a; + --reactpy-color-darker: #34743b; + --reactpy-color-opacity-10: rgba(88, 185, 98, 0.1); +} + +[data-md-color-accent="red"] { + --md-primary-fg-color--light: var(--reactpy-color); + --md-primary-fg-color--dark: var(--reactpy-color-dark); +} + +[data-md-color-scheme="slate"] { + --md-default-bg-color: rgb(35, 39, 47); + --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54); + --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26); + --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07); + --md-primary-fg-color: var(--md-default-bg-color); + --md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1); + --md-default-fg-color--light: #fff; + --md-typeset-a-color: var(--reactpy-color); + --md-accent-fg-color: var(--reactpy-color-dark); +} + +[data-md-color-scheme="default"] { + --md-primary-fg-color: var(--md-default-bg-color); + --md-default-fg-color--light: #000; + --md-default-fg-color--lighter: #0000007e; + --md-default-fg-color--lightest: #00000029; + --md-typeset-color: rgb(35, 39, 47); + --md-typeset-a-color: var(--reactpy-color); + --md-accent-fg-color: var(--reactpy-color-dark); +} + +/* Font changes */ +.md-typeset { + font-weight: 300; +} + +.md-typeset h1 { + font-weight: 600; + margin: 0; + font-size: 2.5em; +} + +.md-typeset h2 { + font-weight: 500; +} + +.md-typeset h3 { + font-weight: 400; +} + +/* Intro section styling */ +p.intro { + font-size: 0.9rem; + font-weight: 500; +} + +/* Hide "Overview" jump selector */ +h2#overview { + visibility: hidden; + height: 0; + margin: 0; + padding: 0; +} + +/* Reduce size of the outdated banner */ +.md-banner__inner { + margin: 0.45rem auto; +} + +/* Desktop Styles */ +@media screen and (min-width: 60em) { + /* Remove max width on desktop */ + .md-grid { + max-width: none; + } +} + +/* Max size of page content */ +.md-content { + max-width: 56rem; +} diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css new file mode 100644 index 0000000..33e8b14 --- /dev/null +++ b/docs/src/assets/css/navbar.css @@ -0,0 +1,185 @@ +[data-md-color-scheme="slate"] { + --md-header-border-color: rgb(255 255 255 / 5%); + --md-version-bg-color: #ffffff0d; +} + +[data-md-color-scheme="default"] { + --md-header-border-color: rgb(0 0 0 / 7%); + --md-version-bg-color: #ae58ee2e; +} + +.md-header { + border: 0 solid transparent; + border-bottom-width: 1px; +} + +.md-header--shadow { + box-shadow: none; + border-color: var(--md-header-border-color); + transition: border-color 0.35s cubic-bezier(0.1, 0.7, 0.1, 1); +} + +/* Version selector */ +.md-header__topic .md-ellipsis, +.md-header__title [data-md-component="header-topic"] { + display: none; +} + +[dir="ltr"] .md-version__current { + margin: 0; +} + +.md-version__list { + margin: 0; + left: 0; + right: 0; + top: 2.5rem; +} + +.md-version { + background: var(--md-version-bg-color); + border-radius: 999px; + padding: 0 0.8rem; + margin: 0.3rem 0; + height: 1.8rem; + display: flex; + font-size: 0.7rem; +} + +/* Mobile Styling */ +@media screen and (max-width: 60em) { + label.md-header__button.md-icon[for="__drawer"] { + order: 1; + } + .md-header__button.md-logo { + display: initial; + order: 2; + margin-right: auto; + } + .md-header__title { + order: 3; + } + .md-header__button[for="__search"] { + order: 4; + } + .md-header__option[data-md-component="palette"] { + order: 5; + } + .md-header__source { + display: initial; + order: 6; + } + .md-header__source .md-source__repository { + display: none; + } +} + +/* Desktop Styling */ +@media screen and (min-width: 60em) { + /* Nav container */ + nav.md-header__inner { + display: contents; + } + header.md-header { + display: flex; + align-items: center; + } + + /* Logo */ + .md-header__button.md-logo { + order: 1; + padding-right: 0.4rem; + padding-top: 0; + padding-bottom: 0; + } + .md-header__button.md-logo img { + height: 2rem; + } + + /* Version selector */ + [dir="ltr"] .md-header__title { + order: 2; + margin: 0; + margin-right: 0.8rem; + margin-left: 0.2rem; + flex-grow: 0; + } + .md-header__topic { + position: relative; + } + .md-header__title--active .md-header__topic { + transform: none; + opacity: 1; + pointer-events: auto; + z-index: 4; + } + + /* Search */ + .md-search { + order: 3; + width: 100%; + margin-right: 0.6rem; + } + .md-search__inner { + width: 100%; + float: unset !important; + } + .md-search__form { + border-radius: 9999px; + } + [data-md-toggle="search"]:checked ~ .md-header .md-header__option { + max-width: unset; + opacity: unset; + transition: unset; + } + + /* Tabs */ + .md-tabs { + order: 4; + min-width: -webkit-fit-content; + min-width: -moz-fit-content; + min-width: fit-content; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + z-index: -1; + overflow: visible; + border: none !important; + } + li.md-tabs__item.md-tabs__item--active { + background: var(--reactpy-color-opacity-10); + border-radius: 9999px; + color: var(--md-typeset-a-color); + } + .md-tabs__link { + margin: 0; + } + .md-tabs__item { + height: 1.8rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + + /* Dark/Light Selector */ + .md-header__option[data-md-component="palette"] { + order: 5; + } + + /* GitHub info */ + .md-header__source { + order: 6; + margin-left: 0 !important; + } +} + +/* Ultrawide Desktop Styles */ +@media screen and (min-width: 1919px) { + .md-search { + order: 2; + width: 100%; + max-width: 34.4rem; + margin: 0 auto; + } +} diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css new file mode 100644 index 0000000..b6507d9 --- /dev/null +++ b/docs/src/assets/css/sidebar.css @@ -0,0 +1,104 @@ +:root { + --sizebar-font-size: 0.62rem; +} + +.md-nav__link { + word-break: break-word; +} + +/* Desktop Styling */ +@media screen and (min-width: 76.1875em) { + /* Move the sidebar and TOC to the edge of the page */ + .md-main__inner.md-grid { + margin-left: 0; + margin-right: 0; + max-width: unset; + display: grid; + grid-template-columns: auto 1fr auto; + } + + .md-content { + justify-self: center; + width: 100%; + } + /* Made the sidebar buttons look React-like */ + .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { + text-transform: uppercase; + } + + .md-nav__title[for="__toc"] { + text-transform: uppercase; + margin: 0.5rem; + } + + .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { + color: rgb(133, 142, 159); + margin: 0.5rem; + } + + .md-nav__item .md-nav__link { + position: relative; + } + + .md-nav__link:is(:focus, :hover):not(.md-nav__link--active) { + color: unset; + } + + .md-nav__item + .md-nav__link:is(:focus, :hover):not(.md-nav__link--active):before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.2; + z-index: -1; + background: grey; + } + + .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: var(--reactpy-color-opacity-10); + } + + .md-nav__link { + padding: 0.5rem 0.5rem 0.5rem 1rem; + margin: 0; + border-radius: 0 10px 10px 0; + font-weight: 500; + overflow: hidden; + font-size: var(--sizebar-font-size); + } + + .md-sidebar__scrollwrap { + margin: 0; + } + + [dir="ltr"] + .md-nav--lifted + .md-nav[data-md-level="1"] + > .md-nav__list + > .md-nav__item { + padding: 0; + } + + .md-nav__item--nested .md-nav__item .md-nav__item { + padding: 0; + } + + .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { + font-weight: 300; + } + + .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { + font-weight: 400; + padding-left: 1.25rem; + } +} diff --git a/docs/src/assets/css/table-of-contents.css b/docs/src/assets/css/table-of-contents.css new file mode 100644 index 0000000..6c94f06 --- /dev/null +++ b/docs/src/assets/css/table-of-contents.css @@ -0,0 +1,48 @@ +/* Table of Contents styling */ +@media screen and (min-width: 60em) { + [data-md-component="sidebar"] .md-nav__title[for="__toc"] { + text-transform: uppercase; + margin: 0.5rem; + margin-left: 0; + font-size: var(--sizebar-font-size); + } + + [data-md-component="toc"] .md-nav__item .md-nav__link--active { + position: relative; + } + + [data-md-component="toc"] .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.15; + z-index: -1; + background: var(--md-typeset-a-color); + } + + [data-md-component="toc"] .md-nav__link { + padding: 0.5rem 0.5rem; + margin: 0; + border-radius: 10px 0 0 10px; + font-weight: 400; + } + + [data-md-component="toc"] + .md-nav__item + .md-nav__list + .md-nav__item + .md-nav__link { + padding-left: 1.25rem; + } + + [dir="ltr"] .md-sidebar__inner { + padding: 0; + } + + .md-nav__item { + padding: 0; + } +} diff --git a/docs/src/assets/js/main.js b/docs/src/assets/js/main.js new file mode 100644 index 0000000..50e2dda --- /dev/null +++ b/docs/src/assets/js/main.js @@ -0,0 +1,19 @@ +// Sync scrolling between the code node and the line number node +// Event needs to be a separate function, otherwise the event will be triggered multiple times +let code_with_lineno_scroll_event = function () { + let tr = this.parentNode.parentNode.parentNode.parentNode; + let lineno = tr.querySelector(".linenos"); + lineno.scrollTop = this.scrollTop; +}; + +const observer = new MutationObserver((mutations) => { + let lineno = document.querySelectorAll(".linenos~.code"); + lineno.forEach(function (element) { + let code = element.parentNode.querySelector("code"); + code.addEventListener("scroll", code_with_lineno_scroll_event); + }); +}); + +observer.observe(document.body, { + childList: true, +}); diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg deleted file mode 100644 index 312fb87..0000000 --- a/docs/src/assets/logo.svg +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/src/contributing.md b/docs/src/contributing.md deleted file mode 100644 index 520531f..0000000 --- a/docs/src/contributing.md +++ /dev/null @@ -1,70 +0,0 @@ -# Contributing - -!!! note - - The [Code of Conduct](https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md) - applies in all community spaces. If you are not familiar with our Code of Conduct policy, - take a minute to read it before making your first contribution. - -The ReactPy team welcomes contributions and contributors of all kinds - whether they -come as code changes, participation in the discussions, opening issues and pointing out -bugs, or simply sharing your work with your colleagues and friends. We’re excited to see -how you can help move this project and community forward! - -## Everyone Can Contribute! - -Trust us, there’s so many ways to support the project. We’re always looking for people who can: - -- Improve our documentation -- Teach and tell others about ReactPy -- Share ideas for new features -- Report bugs -- Participate in general discussions - -Still aren’t sure what you have to offer? Just [ask us](https://github.com/reactive-python/reactpy-router/discussions) and we’ll help you make your first contribution. - -## Development Environment - -For a developer installation from source be sure to install -[NPM](https://www.npmjs.com/) before running: - -```bash -git clone https://github.com/reactive-python/reactpy-router -cd reactpy-router -pip install -e . -r requirements.txt -``` - -This will install an ediable version of `reactpy-router` as well as tools you'll need -to work with this project. - -Of particular note is [`nox`](https://nox.thea.codes/en/stable/), which is used to -automate testing and other development tasks. - -## Running the Tests - -```bash -nox -t test -``` - -You can run the tests with a headed browser. - -```bash -nox -t test -- --headed -``` - -## Releasing This Package - -To release a new version of reactpy-router on PyPI: - -1. Install [`twine`](https://twine.readthedocs.io/en/latest/) with `pip install twine` -2. Update the `version = "x.y.z"` variable in `reactpy-router/__init__.py` -3. `git` add the changes to `__init__.py` and create a `git tag -a x.y.z -m 'comment'` -4. Build the Python package with `python setup.py sdist bdist_wheel` -5. Check the build artifacts `twine check --strict dist/*` -6. Upload the build artifacts to [PyPI](https://pypi.org/) `twine upload dist/*` - -To release a new version of `reactpy-router` on [NPM](https://www.npmjs.com/): - -1. Update `js/package.json` with new npm package version -2. Clean out prior builds `git clean -fdx` -3. Install and publish `npm install && npm publish` diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt new file mode 100644 index 0000000..6eb9552 --- /dev/null +++ b/docs/src/dictionary.txt @@ -0,0 +1,39 @@ +sanic +plotly +nox +WebSocket +WebSockets +changelog +async +pre +prefetch +prefetching +preloader +whitespace +refetch +refetched +refetching +html +jupyter +iframe +keyworded +stylesheet +stylesheets +unstyled +py +reactpy +asgi +postfixed +postprocessing +serializable +postprocessor +preprocessor +middleware +backends +backend +frontend +frontends +misconfiguration +misconfigurations +backhaul +sublicense diff --git a/docs/src/index.md b/docs/src/index.md index 351fd71..3630a87 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,18 +1,11 @@ -# ReactPy Router +## Install from PyPI -A URL router for [ReactPy](https://reactpy.dev). +Run the following command to install [`reactpy-router`](https://pypi.org/project/reactpy-router/) in your Python environment. -!!! note - - If you don't already know the basics of working with ReactPy, you should - [start there](https://reactpy.dev/docs/guides/getting-started/index.html). - -## Installation - -Use `pip` to install this package: - -```bash +```bash linenums="0" pip install reactpy-router ``` -[installer.records][] +## Done! + +You're now ready to start building your own ReactPy applications with URL routing. diff --git a/docs/src/tutorials/custom-router.md b/docs/src/learn/custom-router.md similarity index 100% rename from docs/src/tutorials/custom-router.md rename to docs/src/learn/custom-router.md diff --git a/docs/src/learn/hooks.md b/docs/src/learn/hooks.md new file mode 100644 index 0000000..3479ffc --- /dev/null +++ b/docs/src/learn/hooks.md @@ -0,0 +1,27 @@ +Several pre-fabricated hooks are provided to help integrate with routing features. You can learn more about them below. + +!!! abstract "Note" + + If you're not familiar what a hook is, you should [read the ReactPy docs](https://reactpy.dev/docs/guides/adding-interactivity/components-with-state/index.html#your-first-hook). + +--- + +## Use Query + +The [`use_query`][src.reactpy_router.use_query] hook can be used to access query parameters from the current location. It returns a dictionary of query parameters, where each value is a list of strings. + +=== "components.py" + + ```python + {% include "../../examples/python/use-query.py" %} + ``` + +## Use Parameters + +The [`use_params`][src.reactpy_router.use_params] hook can be used to access route parameters from the current location. It returns a dictionary of route parameters, where each value is mapped to a value that matches the type specified in the route path. + +=== "components.py" + + ```python + {% include "../../examples/python/use-params.py" %} + ``` diff --git a/docs/src/learn/routers-routes-and-links.md b/docs/src/learn/routers-routes-and-links.md new file mode 100644 index 0000000..af62578 --- /dev/null +++ b/docs/src/learn/routers-routes-and-links.md @@ -0,0 +1,67 @@ +We include built-in components that automatically handle routing, which enable Single Page Application (SPA) behavior. + +--- + +## Routers and Routes + +The [`simple.router`][src.reactpy_router.simple.router] component is one possible implementation of a [Router][src.reactpy_router.types.Router]. Routers takes a series of [route][src.reactpy_router.route] objects as positional arguments and render whatever element matches the current location. + +!!! abstract "Note" + + The current location is determined based on the browser's current URL and can be found + by checking the [`use_location`][reactpy.backend.hooks.use_location] hook. + +Here's a basic example showing how to use `#!python simple.router` with two routes. + +=== "components.py" + + ```python + {% include "../../examples/python/basic-routing.py" %} + ``` + +Here we'll note some special syntax in the route path for the second route. The `#!python "*"` is a wildcard that will match any path. This is useful for creating a "404" page that will be shown when no other route matches. + +### Simple Router + +The syntax for declaring routes with the [simple.router][src.reactpy_router.simple.router] is very similar to the syntax used by [`starlette`](https://www.starlette.io/routing/) (a popular Python web framework). As such route parameters are declared using the following syntax: + +```python linenums="0" +/my/route/{param} +/my/route/{param:type} +``` + +In this case, `#!python param` is the name of the route parameter and the optionally declared `#!python type` specifies what kind of parameter it is. The available parameter types and what patterns they match are are: + +| Type | Pattern | +| --- | --- | +| `#!python str` (default) | `#!python [^/]+` | +| `#!python int` | `#!python \d+` | +| `#!python float` | `#!python \d+(\.\d+)?` | +| `#!python uuid` | `#!python [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` | +| `#!python path` | `#!python .+` | + +So in practice these each might look like: + +```python linenums="0" +/my/route/{param} +/my/route/{param:int} +/my/route/{param:float} +/my/route/{param:uuid} +/my/route/{param:path} +``` + +Any route parameters collected from the current location then be accessed using the [`use_params`](#using-parameters) hook. + +!!! warning "Pitfall" + + While it is possible to use route parameters to capture values from query strings (such as `#!python /my/route/?foo={bar}`), this is not recommended. Instead, you should use the [`use_query`][src.reactpy_router.use_query] hook to access query string values. + +## Route Links + +Links between routes should be created using the [link][src.reactpy_router.link] component. This will allow ReactPy to handle the transition between routes and avoid a page reload. + +=== "components.py" + + ```python + {% include "../../examples/python/route-links.py" %} + ``` diff --git a/docs/src/learn/simple-application.md b/docs/src/learn/simple-application.md new file mode 100644 index 0000000..8f2a5b5 --- /dev/null +++ b/docs/src/learn/simple-application.md @@ -0,0 +1,88 @@ +

+ +Here you'll learn the various features of `reactpy-router` and how to use them. These examples will utilize the [`reactpy_router.simple.router`][src.reactpy_router.simple.router]. + +

+ +!!! abstract "Note" + + These docs assume you already know the basics of [ReacPy](https://reactpy.dev). + +--- + +Let's build a simple web application for viewing messages between several people. + +For the purposes of this tutorial we'll be working with the following data. + +```python linenums="0" +message_data = [ + {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, + {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, + {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, + {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, + {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, + {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, + {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, + {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, +] +``` + +In a more realistic application this data would be stored in a database, but for this tutorial we'll just keep it in memory. + +## Creating Basic Routes + +The first step is to create a basic router that will display the home page when the user navigates to the root of the application, and a "missing link" page for any other route. + +=== "components.py" + + ```python + {% include "../../examples/python/basic-routing.py" %} + ``` + +When navigating to [`http://127.0.0.1:8000``](http://127.0.0.1:8000) you should see `Home Page 🏠`. However, if you go to any other route you will instead see `Missing Link 🔗‍đŸ’Ĩ`. + +With this foundation you can start adding more routes. + +=== "components.py" + + ```python + {% include "../../examples/python/basic-routing-more-routes.py" %} + ``` + +With this change you can now also go to [`/messages`](http://127.0.0.1:8000/messages) to see `Messages đŸ’Ŧ`. + +## Using Route Links + +Instead of using the standard `#!python reactpy.html.a` element to create links to different parts of your application, use `#!python reactpy_router.link` instead. When users click links constructed using `#!python reactpy_router.link`, ReactPy will handle the transition and prevent a full page reload. + +=== "components.py" + + ```python + {% include "../../examples/python/route-links.py" %} + ``` + +Now, when you go to the home page, you can click `Messages` link to go to [`/messages`](http://127.0.0.1:8000/messages). + +## Adding Nested Routes + +Routes can be nested in order to construct more complicated application structures. + +=== "components.py" + + ```python + {% include "../../examples/python/nested-routes.py" %} + ``` + +## Adding Route Parameters + +In the example above we had to manually create a `#!python messages_with(...)` component for each conversation. This would be better accomplished by defining a single route that declares route parameters instead. + +Any parameters that have matched in the currently displayed route can then be consumed with the `#!python use_params` hook which returns a dictionary mapping the parameter names to their values. Note that parameters with a declared type will be converted to is in the parameters dictionary. So for example `#!python /my/route/{my_param:float}` would match `#!python /my/route/3.14` and have a parameter dictionary of `#!python {"my_param": 3.14}`. + +If we take this information and apply it to our growing example application we'd substitute the manually constructed `#!python /messages/with` routes with a single `#!python /messages/with/{names}` route. + +=== "components.py" + + ```python + {% include "../../examples/python/route-parameters.py" %} + ``` diff --git a/docs/src/reference.md b/docs/src/reference.md deleted file mode 100644 index aabc9b3..0000000 --- a/docs/src/reference.md +++ /dev/null @@ -1,5 +0,0 @@ -# Reference - -::: reactpy_router.core -::: reactpy_router.simple -::: reactpy_router.types diff --git a/docs/src/reference/core.md b/docs/src/reference/core.md new file mode 100644 index 0000000..26cf9e5 --- /dev/null +++ b/docs/src/reference/core.md @@ -0,0 +1 @@ +::: src.reactpy_router.core diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md new file mode 100644 index 0000000..2fcea59 --- /dev/null +++ b/docs/src/reference/router.md @@ -0,0 +1 @@ +::: src.reactpy_router.simple diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md new file mode 100644 index 0000000..0482432 --- /dev/null +++ b/docs/src/reference/types.md @@ -0,0 +1 @@ +::: src.reactpy_router.types diff --git a/docs/src/tutorials/simple-app.md b/docs/src/tutorials/simple-app.md deleted file mode 100644 index 5f7fcbd..0000000 --- a/docs/src/tutorials/simple-app.md +++ /dev/null @@ -1,277 +0,0 @@ -# Simple Application - -Let's build a simple web application for viewing messages between several people. - -For the purposes of this tutorial we'll be working with the following data: - -```python -message_data = [ - {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, - {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, - {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, - {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, - {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, - {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, - {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, - {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, -] -``` - -In a more realistic application this data would be stored in a database, but for this -tutorial we'll just keep it in memory. - -## Basic Routing - -The first step is to create a basic router that will display the home page when the -user navigates to the root of the application, and a "missing link" page for any other -route: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple - -@component -def root(): - return simple.router( - route("/", html.h1("Home Page 🏠")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), - ) - -run(root) -``` - -When navigating to http://127.0.0.1:8000 you should see "Home Page 🏠". However, if you -go to any other route (e.g. http://127.0.0.1:8000/missing) you will instead see the -"Missing Link 🔗‍đŸ’Ĩ" page. - -With this foundation you can start adding more routes: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple - -@component -def root(): - return simple.router( - route("/", html.h1("Home Page 🏠")), - route("/messages", html.h1("Messages đŸ’Ŧ")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), - ) - -run(root) -``` - -With this change you can now also go to `/messages` to see "Messages đŸ’Ŧ" displayed. - -## Route Links - -Instead of using the standard `` element to create links to different parts of your -application, use `reactpy_router.link` instead. When users click links constructed using -`reactpy_router.link`, instead of letting the browser navigate to the associated route, -ReactPy will more quickly handle the transition by avoiding the cost of a full page -load. - -```python -from reactpy import component, html, run -from reactpy_router import link, route, simple - -@component -def root(): - return simple.router( - route("/", home()), - route("/messages", html.h1("Messages đŸ’Ŧ")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), - ) - -@component -def home(): - return html.div( - html.h1("Home Page 🏠"), - link("Messages", to="/messages"), - ) - -run(root) -``` - -Now, when you go to the home page, you can click the link to go to `/messages`. - -## Nested Routes - -Routes can be nested in order to construct more complicated application structures: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple, link - -message_data = [ - {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, - {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, - {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, - {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, - {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, - {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, - {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, - {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, -] - -@component -def root(): - return simple.router( - route("/", home()), - route( - "/messages", - all_messages(), - # we'll improve upon these manually created routes in the next section... - route("/with/Alice", messages_with("Alice")), - route("/with/Alice-Bob", messages_with("Alice", "Bob")), - ), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), - ) - -@component -def home(): - return html.div( - html.h1("Home Page 🏠"), - link("Messages", to="/messages"), - ) - -@component -def all_messages(): - last_messages = { - ", ".join(msg["with"]): msg - for msg in sorted(message_data, key=lambda m: m["id"]) - } - return html.div( - html.h1("All Messages đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else '🔴'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), - ) - -@component -def messages_with(*names): - names = set(names) - messages = [msg for msg in message_data if set(msg["with"]) == names] - return html.div( - html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - f"{msg['from'] or 'You'}: {msg['message']}", - ) - for msg in messages - ] - ), - ) - -run(root) -``` - -## Route Parameters - -In the example above we had to manually create a `messages_with(...)` component for each -conversation. This would be better accomplished by defining a single route that declares -["route parameters"](../usage.md#simple-router) instead. - -Any parameters that have matched in the currently displayed route can then be consumed -with the `use_params` hook which returns a dictionary mapping the parameter names to -their values. Note that parameters with a declared type will be converted to is in the -parameters dictionary. So for example `/my/route/{my_param:float}` would match -`/my/route/3.14` and have a parameter dictionary of `{"my_param": 3.14}`. - -If we take this information and apply it to our growing example application we'd -substitute the manually constructed `/messages/with` routes with a single -`/messages/with/{names}` route: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple, link -from reactpy_router.core import use_params - -message_data = [ - {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, - {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, - {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, - {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, - {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, - {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, - {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, - {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, -] - -@component -def root(): - return simple.router( - route("/", home()), - route( - "/messages", - all_messages(), - route("/with/{names}", messages_with()), # note the path param - ), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), - ) - -@component -def home(): - return html.div( - html.h1("Home Page 🏠"), - link("Messages", to="/messages"), - ) - -@component -def all_messages(): - last_messages = { - ", ".join(msg["with"]): msg - for msg in sorted(message_data, key=lambda m: m["id"]) - } - return html.div( - html.h1("All Messages đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else '🔴'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), - ) - -@component -def messages_with(): - names = set(use_params()["names"].split("-")) # and here we use the path param - messages = [msg for msg in message_data if set(msg["with"]) == names] - return html.div( - html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - f"{msg['from'] or 'You'}: {msg['message']}", - ) - for msg in messages - ] - ), - ) - -run(root) -``` diff --git a/docs/src/usage.md b/docs/src/usage.md deleted file mode 100644 index e0f7bb2..0000000 --- a/docs/src/usage.md +++ /dev/null @@ -1,180 +0,0 @@ -# Usage - -!!! note - - The sections below assume you already know the basics of [ReacPy](https://reactpy.dev). - -Here you'll learn the various features of `reactpy-router` and how to use them. All examples -will utilize the [simple.router][reactpy_router.simple.router] (though you can [use your own](#custom-routers)). - -## Routers and Routes - -The [simple.router][reactpy_router.simple.router] component is one possible -implementation of a [Router][reactpy_router.types.Router]. Routers takes a series of -[Route][reactpy_router.types.Route] objects as positional arguments and render whatever -element matches the current location. For convenience, these `Route` objects are created -using the [route][reactpy_router.route] function. - -!!! note - - The current location is determined based on the browser's current URL and can be found - by checking the [use_location][reactpy.backend.hooks.use_location] hook. - -Here's a basic example showing how to use `simple.router` with two routes: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple, use_location - -@component -def root(): - location = use_location() - return simple.router( - route("/", html.h1("Home Page 🏠")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), - ) -``` - -Here we'll note some special syntax in the route path for the second route. The `*` is a -wildcard that will match any path. This is useful for creating a "404" page that will be -shown when no other route matches. - -### Simple Router - -The syntax for declaring routes with the [simple.router][reactpy_router.simple.router] -is very similar to the syntax used by [Starlette](https://www.starlette.io/routing/) (a -popular Python web framework). As such route parameters are declared using the following -syntax: - -``` -/my/route/{param} -/my/route/{param:type} -``` - -In this case, `param` is the name of the route parameter and the optionally declared -`type` specifies what kind of parameter it is. The available parameter types and what -patterns they match are are: - -- str (default) - `[^/]+` -- int - `\d+` -- float - `\d+(\.\d+)?` -- uuid - `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` -- path - `.+` - -!!! note - - The `path` type is special in that it will match any path, including `/` characters. - This is useful for creating routes that match a path prefix. - -So in practice these each might look like: - -``` -/my/route/{param} -/my/route/{param:int} -/my/route/{param:float} -/my/route/{param:uuid} -/my/route/{param:path} -``` - -Any route parameters collected from the current location then be accessed using the -[`use_params`](#using-parameters) hook. - -!!! note - - It's worth pointing out that, while you can use route parameters to capture values - from queryies (i.e. `?foo=bar`), this is not recommended. Instead, you should use - the [use_query][reactpy_router.use_query] hook to access query parameters. - -### Route Links - -Links between routes should be created using the [link][reactpy_router.link] component. -This will allow ReactPy to handle the transition between routes more quickly by avoiding -the cost of a full page load. - -```python -from reactpy import component, html, run, use_location -from reactpy_router import link, route, simple - -@component -def root(): - use_location() - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link(html.button("About"), to="/about"), - ), - ), - route("/about", html.h1("About Page 📖")), - ) -``` - -## Hooks - -`reactpy-router` provides a number of hooks for working with the routes: - -- [`use_query`](#using-queries) - for accessing query parameters -- [`use_params`](#using-parameters) - for accessing route parameters - -If you're not familiar with hooks, you should -[read the docs](https://reactpy.dev/docs/guides/adding-interactivity/components-with-state/index.html#your-first-hook). - -### Using Queries - -The [use_query][reactpy_router.use_query] hook can be used to access query parameters -from the current location. It returns a dictionary of query parameters, where each value -is a list of strings. - -```python -from reactpy import component, html, run -from reactpy_router import link, route, simple, use_query - -@component -def root(): - use_location() - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link("Search", to="/search?q=reactpy"), - ), - ), - route("/about", html.h1("About Page 📖")), - ) - -@component -def search(): - query = use_query() - return html.h1(f"Search Results for {query['q'][0]} 🔍") -``` - -### Using Parameters - -The [use_params][reactpy_router.use_params] hook can be used to access route parameters -from the current location. It returns a dictionary of route parameters, where each value -is mapped to a value that matches the type specified in the route path. - -```python -from reactpy import component, html, run -from reactpy_router import link, route, simple, use_params - -@component -def root(): - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link("User 123", to="/user/123"), - ), - ), - route("/user/{id:int}", user()), - ) - -@component -def user(): - params = use_params() - return html.h1(f"User {params['id']} 👤") -``` diff --git a/js/README.md b/js/README.md deleted file mode 100644 index 1d6fc22..0000000 --- a/js/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# reactpy-router - -A URL router for ReactPy - -# Package Installation - -Requires [Node](https://nodejs.org/en/) to be installed: - -```bash -npm install --save reactpy-router -``` - -For a developer installation, `cd` into this directory and run: - -```bash -npm install -npm run build -``` - -This will install required dependencies and generate a Javascript bundle that is saved -to `reactpy-router/bundle.js`` and is distributed with the -associated Python package. diff --git a/noxfile.py b/noxfile.py index 1fddabb..1eebea2 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,73 +2,51 @@ from nox import Session, session -ROOT = Path(".") -REQUIREMENTS_DIR = ROOT / "requirements" - - -@session -def format(session: Session) -> None: - install_requirements(session, "check-style") - session.run("black", ".") - session.run("isort", ".") - - -@session -def docs(session: Session) -> None: - setup_docs(session) - session.run("mkdocs", "serve") - - -@session -def docs_build(session: Session) -> None: - setup_docs(session) - session.run("mkdocs", "build") - - -@session(tags=["test"]) -def test_style(session: Session) -> None: - install_requirements(session, "check-style") - session.run("black", "--check", ".") - session.run("isort", "--check", ".") - session.run("flake8", ".") - - -@session(tags=["test"]) -def test_types(session: Session) -> None: - install_requirements(session, "check-types") - session.run("mypy", "--strict", "reactpy_router") +ROOT_DIR = Path(__file__).parent @session(tags=["test"]) -def test_suite(session: Session) -> None: - install_requirements(session, "test-env") +def test_python(session: Session) -> None: + install_requirements_file(session, "test-env") + session.install(".[all]") session.run("playwright", "install", "chromium") posargs = session.posargs[:] - if "--no-cov" in session.posargs: - posargs.remove("--no-cov") - session.log("Coverage won't be checked") - session.install(".") - else: + if "--coverage" in posargs: posargs += ["--cov=reactpy_router", "--cov-report=term"] + posargs.remove("--coverage") session.install("-e", ".") + else: + session.log("Coverage won't be checked unless `-- --coverage` is defined.") session.run("pytest", "tests", *posargs) @session(tags=["test"]) -def test_javascript(session: Session) -> None: - session.chdir(ROOT / "js") - session.run("npm", "install", external=True) - session.run("npm", "run", "check") +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_router", "tests") -def setup_docs(session: Session) -> None: - install_requirements(session, "build-docs") - session.install("-e", ".") - session.chdir("docs") +@session(tags=["test"]) +def test_style(session: Session) -> None: + install_requirements_file(session, "check-style") + session.run("black", ".", "--check") + 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(session: Session, name: str) -> None: - session.install("-r", str(REQUIREMENTS_DIR / f"{name}.txt")) +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 d645c38..763f3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,22 @@ [build-system] -requires = ["setuptools>=40.8.0", "wheel"] +requires = ["setuptools>=42", "wheel", "nodejs-bin==18.4.0a4"] build-backend = "setuptools.build_meta" - -[tool.pytest.ini_options] -testpaths = "tests" -asyncio_mode = "auto" - - -[tool.isort] -profile = "black" - - [tool.mypy] ignore_missing_imports = true warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true +check_untyped_defs = true + +[tool.ruff.isort] +known-first-party = ["src", "tests"] + +[tool.ruff] +ignore = ["E501"] +extend-exclude = [".venv/*", ".eggs/*", ".nox/*", "build/*"] +line-length = 120 + +[tool.pytest.ini_options] +testpaths = "tests" +asyncio_mode = "auto" diff --git a/requirements.txt b/requirements.txt index 5893bf2..7c5eed9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -r requirements/build-docs.txt +-r requirements/build-pkg.txt -r requirements/check-style.txt -r requirements/check-types.txt --r requirements/nox-deps.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 index 3f19165..0d2bca2 100644 --- a/requirements/build-docs.txt +++ b/requirements/build-docs.txt @@ -1,5 +1,11 @@ mkdocs -mkdocs-material -mkdocs-gen-files -mkdocs-literate-nav +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 mkdocstrings[python] diff --git a/requirements/build-pkg.txt b/requirements/build-pkg.txt new file mode 100644 index 0000000..88ec271 --- /dev/null +++ b/requirements/build-pkg.txt @@ -0,0 +1,3 @@ +twine +wheel +setuptools diff --git a/requirements/check-style.txt b/requirements/check-style.txt index 9a48a39..e4f6562 100644 --- a/requirements/check-style.txt +++ b/requirements/check-style.txt @@ -1,5 +1,2 @@ -black -flake8 -flake8-print -reactpy-flake8 -isort +black >=23,<24 +ruff diff --git a/requirements/test-env.txt b/requirements/test-env.txt index b6cdf49..4ddd635 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -3,3 +3,4 @@ pytest pytest-asyncio pytest-cov reactpy[testing,starlette] +nodejs-bin==18.4.0a4 diff --git a/requirements/nox-deps.txt b/requirements/test-run.txt similarity index 100% rename from requirements/nox-deps.txt rename to requirements/test-run.txt diff --git a/setup.cfg b/setup.cfg index 8d1ef40..e7be17a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,15 +1,6 @@ [bdist_wheel] universal=1 -[flake8] -ignore = E203, E266, E501, W503, F811, N802 -max-line-length = 88 -extend-exclude = - .nox - venv - .venv - tests/cases/* - [coverage:report] fail_under = 100 show_missing = True diff --git a/setup.py b/setup.py index f3c5f79..c643fa1 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,33 @@ from __future__ import print_function -import os -import shutil -import subprocess import sys +import traceback +from distutils import log +from pathlib import Path -from setuptools import find_packages, setup +from nodejs import npm +from setuptools import find_namespace_packages, setup from setuptools.command.develop import develop from setuptools.command.sdist import sdist -# the name of the project +# ----------------------------------------------------------------------------- +# Basic Constants +# ----------------------------------------------------------------------------- name = "reactpy_router" - -# basic paths used to gather files -here = os.path.abspath(os.path.dirname(__file__)) -package_dir = os.path.join(here, name) +root_dir = Path(__file__).parent +src_dir = root_dir / "src" +package_dir = src_dir / name # ----------------------------------------------------------------------------- # General Package Info # ----------------------------------------------------------------------------- - - package = { "name": name, "python_requires": ">=3.9", - "packages": find_packages(exclude=["tests*"]), - "description": "A URL router for ReactPy", + "packages": find_namespace_packages(src_dir), + "package_dir": {"": "src"}, + "description": "A URL router for ReactPy.", "author": "Ryan Morshead", "author_email": "ryan.morshead@gmail.com", "url": "https://github.com/reactive-python/reactpy-router", @@ -38,9 +39,9 @@ "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", "Typing :: Typed", @@ -49,38 +50,30 @@ # ----------------------------------------------------------------------------- -# Requirements +# Library Version # ----------------------------------------------------------------------------- - - -requirements = [] -with open(os.path.join(here, "requirements", "pkg-deps.txt"), "r") as f: - for line in map(str.strip, f): - if not line.startswith("#"): - requirements.append(line) -package["install_requires"] = requirements +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) # ----------------------------------------------------------------------------- -# Library Version +# Requirements # ----------------------------------------------------------------------------- - -with open(os.path.join(package_dir, "__init__.py")) as init_file: - for line in init_file: - if line.split("=", 1)[0].strip() == "__version__": - package["version"] = eval(line.split("=", 1)[1]) - break - else: - print("No version found in %s/__init__.py" % package_dir) # noqa: T201 - sys.exit(1) +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 open(os.path.join(here, "README.md")) as f: +with (root_dir / "README.md").open() as f: long_description = f.read() package["long_description"] = long_description @@ -90,16 +83,26 @@ # ---------------------------------------------------------------------------- # Build Javascript # ---------------------------------------------------------------------------- - - -def build_javascript_first(cls): - class Command(cls): +def build_javascript_first(build_cls: type): + class Command(build_cls): def run(self): - npm = shutil.which("npm") # this is required on windows - if npm is None: - raise RuntimeError("NPM is not installed.") - for cmd_str in [f"{npm} install", f"{npm} run build"]: - subprocess.check_call(cmd_str.split(), cwd=os.path.join(here, "js")) + js_dir = str(src_dir / "js") + + log.info("Installing Javascript...") + result = npm.call(["install"], cwd=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=js_dir) + if result != 0: + log.error(traceback.format_exc()) + log.error("Failed to build Javascript") + raise RuntimeError("Failed to build Javascript") + + log.info("Successfully built Javascript") super().run() return Command @@ -121,9 +124,7 @@ def run(self): # ----------------------------------------------------------------------------- -# Install It +# Installation # ----------------------------------------------------------------------------- - - if __name__ == "__main__": setup(**package) diff --git a/js/.eslintrc.json b/src/js/.eslintrc.json similarity index 100% rename from js/.eslintrc.json rename to src/js/.eslintrc.json diff --git a/js/package-lock.json b/src/js/package-lock.json similarity index 100% rename from js/package-lock.json rename to src/js/package-lock.json diff --git a/js/package.json b/src/js/package.json similarity index 100% rename from js/package.json rename to src/js/package.json diff --git a/js/rollup.config.js b/src/js/rollup.config.js similarity index 100% rename from js/rollup.config.js rename to src/js/rollup.config.js diff --git a/js/src/index.js b/src/js/src/index.js similarity index 100% rename from js/src/index.js rename to src/js/src/index.js diff --git a/reactpy_router/__init__.py b/src/reactpy_router/__init__.py similarity index 100% rename from reactpy_router/__init__.py rename to src/reactpy_router/__init__.py diff --git a/reactpy_router/core.py b/src/reactpy_router/core.py similarity index 98% rename from reactpy_router/core.py rename to src/reactpy_router/core.py index 9d8f7a2..490a78c 100644 --- a/reactpy_router/core.py +++ b/src/reactpy_router/core.py @@ -19,7 +19,7 @@ from reactpy.backend.hooks import ConnectionContext, use_connection from reactpy.backend.types import Connection, Location from reactpy.core.types import VdomChild, VdomDict -from reactpy.types import ComponentType, Context, Location +from reactpy.types import ComponentType, Context from reactpy.web.module import export, module_from_file from reactpy_router.types import Route, RouteCompiler, Router, RouteResolver diff --git a/reactpy_router/py.typed b/src/reactpy_router/py.typed similarity index 100% rename from reactpy_router/py.typed rename to src/reactpy_router/py.typed diff --git a/reactpy_router/simple.py b/src/reactpy_router/simple.py similarity index 100% rename from reactpy_router/simple.py rename to src/reactpy_router/simple.py diff --git a/reactpy_router/types.py b/src/reactpy_router/types.py similarity index 100% rename from reactpy_router/types.py rename to src/reactpy_router/types.py diff --git a/tests/test_core.py b/tests/test_core.py index 5f05f5c..77577b3 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,7 @@ +from typing import Any + from reactpy import Ref, component, html, use_location from reactpy.testing import DisplayFixture - from reactpy_router import link, route, simple, use_params, use_query @@ -39,7 +40,7 @@ def sample(): root_element = await display.root_element() except AttributeError: root_element = await display.page.wait_for_selector( - f"#display-{display._next_view_id}", state="attached" + f"#display-{display._next_view_id}", state="attached" # type: ignore ) assert not await root_element.inner_html() @@ -99,7 +100,7 @@ def sample(): async def test_use_params(display: DisplayFixture): - expected_params = {} + expected_params: dict[str, Any] = {} @component def check_params(): @@ -135,7 +136,7 @@ def sample(): async def test_use_query(display: DisplayFixture): - expected_query = {} + expected_query: dict[str, Any] = {} @component def check_query(): From 0fe17fb8a824659fa82d1719514e1f305e469c1c Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 14 Oct 2024 02:03:03 -0700 Subject: [PATCH 06/22] Rewrite ReactPy-Router (#30) ### Changed - Bump GitHub workflows - Rename `use_query` to `use_search_params`. - Rename `simple.router` to `browser_router`. - Rename `SimpleResolver` to `StarletteResolver`. - Rename `CONVERSION_TYPES` to `CONVERTERS`. - Change "Match Any" syntax from a star `*` to `{name:any}`. - Rewrite `reactpy_router.link` to be a server-side component. - Simplified top-level exports within `reactpy_router`. ### Added - New error for ReactPy router elements being used outside router context. - Configurable/inheritable `Resolver` base class. - Add debug log message for when there are no router matches. - Add slug as a supported type. ### Fixed - Fix bug where changing routes could cause render failure due to key identity. - Fix bug where "Match Any" pattern wouldn't work when used in complex or nested paths. - Fix bug where `link` elements could not have `@component` type children. - Fix bug where the ReactPy would not detect the current URL after a reconnection. - Fixed flakey tests on GitHub CI by adding click delays. --- .editorconfig | 32 +++ .github/workflows/codeql.yml | 108 ++++---- .github/workflows/publish-develop-docs.yml | 4 +- .github/workflows/publish-py.yaml | 4 +- .github/workflows/publish-release-docs.yml | 4 +- .github/workflows/test-docs.yml | 66 ++--- .github/workflows/test-src.yaml | 8 +- .gitignore | 4 +- .prettierrc | 4 + CHANGELOG.md | 26 +- MANIFEST.in | 2 +- .../python/basic-routing-more-routes.py | 7 +- docs/examples/python/basic-routing.py | 7 +- docs/examples/python/nested-routes.py | 42 ++- docs/examples/python/route-links.py | 8 +- docs/examples/python/route-parameters.py | 44 ++-- docs/examples/python/use-params.py | 13 +- docs/examples/python/use-query.py | 23 -- docs/examples/python/use-search-params.py | 26 ++ docs/mkdocs.yml | 240 +++++++++--------- .../add-interactivity-demo.html | 172 ------------- .../home-code-examples/add-interactivity.py | 30 --- .../home-code-examples/code-block.html | 7 - .../create-user-interfaces-demo.html | 24 -- .../create-user-interfaces.py | 22 -- .../write-components-with-python-demo.html | 65 ----- .../write-components-with-python.py | 15 -- docs/src/about/code.md | 6 +- docs/src/learn/hooks.md | 8 +- docs/src/learn/routers-routes-and-links.md | 18 +- ...imple-application.md => your-first-app.md} | 4 +- docs/src/reference/components.md | 4 + docs/src/reference/core.md | 1 - docs/src/reference/hooks.md | 4 + docs/src/reference/router.md | 5 +- docs/src/reference/types.md | 2 +- noxfile.py | 1 - pyproject.toml | 6 +- requirements/check-style.txt | 1 - src/js/package-lock.json | 11 - src/js/package.json | 3 +- src/js/rollup.config.js | 2 +- src/js/src/index.js | 66 +++-- src/reactpy_router/__init__.py | 16 +- src/reactpy_router/components.py | 101 ++++++++ src/reactpy_router/converters.py | 37 +++ src/reactpy_router/core.py | 144 ----------- src/reactpy_router/hooks.py | 69 +++++ src/reactpy_router/resolvers.py | 81 ++++++ src/reactpy_router/routers.py | 110 ++++++++ src/reactpy_router/simple.py | 98 ------- src/reactpy_router/static/link.js | 8 + src/reactpy_router/types.py | 51 ++-- tests/conftest.py | 31 ++- tests/test_core.py | 123 ++++++--- tests/test_resolver.py | 57 +++++ tests/test_simple.py | 54 ---- 57 files changed, 1045 insertions(+), 1084 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierrc delete mode 100644 docs/examples/python/use-query.py create mode 100644 docs/examples/python/use-search-params.py delete mode 100644 docs/overrides/home-code-examples/add-interactivity-demo.html delete mode 100644 docs/overrides/home-code-examples/add-interactivity.py delete mode 100644 docs/overrides/home-code-examples/code-block.html delete mode 100644 docs/overrides/home-code-examples/create-user-interfaces-demo.html delete mode 100644 docs/overrides/home-code-examples/create-user-interfaces.py delete mode 100644 docs/overrides/home-code-examples/write-components-with-python-demo.html delete mode 100644 docs/overrides/home-code-examples/write-components-with-python.py rename docs/src/learn/{simple-application.md => your-first-app.md} (93%) create mode 100644 docs/src/reference/components.md delete mode 100644 docs/src/reference/core.md create mode 100644 docs/src/reference/hooks.md create mode 100644 src/reactpy_router/components.py create mode 100644 src/reactpy_router/converters.py delete mode 100644 src/reactpy_router/core.py create mode 100644 src/reactpy_router/hooks.py create mode 100644 src/reactpy_router/resolvers.py create mode 100644 src/reactpy_router/routers.py delete mode 100644 src/reactpy_router/simple.py create mode 100644 src/reactpy_router/static/link.js create mode 100644 tests/test_resolver.py delete mode 100644 tests/test_simple.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..356385d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# 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 + +[*.md] +indent_size = 4 + +[*.html] +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 0f26793..213f18a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,67 +12,51 @@ name: "CodeQL" on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - # Runs at 22:21 on Monday. - - cron: '21 22 * * 1' + push: + branches: ["main"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["main"] + schedule: + # Runs at 22:21 on Monday. + - cron: "21 22 * * 1" jobs: - analyze: - name: Analyze - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript', 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # 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 - - # â„šī¸ 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 - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript", "python"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + 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. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + 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 6b1d4de..95d98da 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -7,10 +7,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install -r requirements/build-docs.txt diff --git a/.github/workflows/publish-py.yaml b/.github/workflows/publish-py.yaml index 34ae5fa..d7437d7 100644 --- a/.github/workflows/publish-py.yaml +++ b/.github/workflows/publish-py.yaml @@ -11,9 +11,9 @@ jobs: publish-package: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install dependencies diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml index 6fc3233..a98e986 100644 --- a/.github/workflows/publish-release-docs.yml +++ b/.github/workflows/publish-release-docs.yml @@ -8,10 +8,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install -r requirements/build-docs.txt diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index d5f5052..7110bc4 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -1,37 +1,39 @@ name: Test on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "0 0 * * *" + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - 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 - - 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/ - black docs/examples/python/ --check - ruff check docs/examples/python/ + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: | + pip install -r requirements/build-docs.txt + pip install -r requirements/check-types.txt + pip install -r requirements/check-style.txt + pip install -e . + - name: Check docs build + run: | + linkcheckMarkdown docs/ -v -r + linkcheckMarkdown README.md -v -r + linkcheckMarkdown CHANGELOG.md -v -r + cd docs + mkdocs build --strict + - name: Check docs examples + run: | + mypy --show-error-codes docs/examples/python/ + ruff check docs/examples/python/ diff --git a/.github/workflows/test-src.yaml b/.github/workflows/test-src.yaml index b5ae7d0..df93152 100644 --- a/.github/workflows/test-src.yaml +++ b/.github/workflows/test-src.yaml @@ -17,9 +17,9 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Python Dependencies @@ -29,9 +29,9 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Latest Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install Python Dependencies diff --git a/.gitignore b/.gitignore index 9155bda..12271fc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ docs/site # --- JAVASCRIPT BUNDLES --- -src/reactpy_router/bundle.js +src/reactpy_router/static/bundle.js # --- PYTHON IGNORE FILES ---- @@ -108,7 +108,7 @@ celerybeat.pid # Environments .env -.venv +.venv* env/ venv/ ENV/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..32ad81f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "never", + "trailingComma": "all" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c673a2f..7380865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,31 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +### Changed + +- Bump GitHub workflows +- Rename `use_query` to `use_search_params`. +- Rename `simple.router` to `browser_router`. +- Rename `SimpleResolver` to `StarletteResolver`. +- Rename `CONVERSION_TYPES` to `CONVERTERS`. +- Change "Match Any" syntax from a star `*` to `{name:any}`. +- Rewrite `reactpy_router.link` to be a server-side component. +- Simplified top-level exports within `reactpy_router`. + +### Added + +- New error for ReactPy router elements being used outside router context. +- Configurable/inheritable `Resolver` base class. +- Add debug log message for when there are no router matches. +- Add slug as a supported type. + +### Fixed + +- Fix bug where changing routes could cause render failure due to key identity. +- Fix bug where "Match Any" pattern wouldn't work when used in complex or nested paths. +- Fix bug where `link` elements could not have `@component` type children. +- Fix bug where the ReactPy would not detect the current URL after a reconnection. +- Fixed flakey tests on GitHub CI by adding click delays. ## [0.1.1] - 2023-12-13 diff --git a/MANIFEST.in b/MANIFEST.in index bdca1f4..71f4855 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include src/reactpy_router/bundle.js +recursive-include src/reactpy_router/static * include src/reactpy_router/py.typed diff --git a/docs/examples/python/basic-routing-more-routes.py b/docs/examples/python/basic-routing-more-routes.py index 8ddbebb..32bb31e 100644 --- a/docs/examples/python/basic-routing-more-routes.py +++ b/docs/examples/python/basic-routing-more-routes.py @@ -1,14 +1,13 @@ from reactpy import component, html, run - -from reactpy_router import route, simple +from reactpy_router import browser_router, route @component def root(): - return simple.router( + return browser_router( route("/", html.h1("Home Page 🏠")), route("/messages", html.h1("Messages đŸ’Ŧ")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + route("{404:any}", html.h1("Missing Link 🔗‍đŸ’Ĩ")), ) diff --git a/docs/examples/python/basic-routing.py b/docs/examples/python/basic-routing.py index 57b7a37..43c4e65 100644 --- a/docs/examples/python/basic-routing.py +++ b/docs/examples/python/basic-routing.py @@ -1,13 +1,12 @@ from reactpy import component, html, run - -from reactpy_router import route, simple +from reactpy_router import browser_router, route @component def root(): - return simple.router( + return browser_router( route("/", html.h1("Home Page 🏠")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + route("{404:any}", html.h1("Missing Link 🔗‍đŸ’Ĩ")), ) diff --git a/docs/examples/python/nested-routes.py b/docs/examples/python/nested-routes.py index f03a692..01ffb18 100644 --- a/docs/examples/python/nested-routes.py +++ b/docs/examples/python/nested-routes.py @@ -1,7 +1,8 @@ from typing import TypedDict from reactpy import component, html, run -from reactpy_router import link, route, simple + +from reactpy_router import browser_router, link, route message_data: list["MessageDataType"] = [ {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, @@ -17,7 +18,7 @@ @component def root(): - return simple.router( + return browser_router( route("/", home()), route( "/messages", @@ -26,7 +27,7 @@ def root(): route("/with/Alice", messages_with("Alice")), route("/with/Alice-Bob", messages_with("Alice", "Bob")), ), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + route("{404:any}", html.h1("Missing Link 🔗‍đŸ’Ĩ")), ) @@ -34,39 +35,32 @@ def root(): def home(): return html.div( html.h1("Home Page 🏠"), - link("Messages", to="/messages"), + link({"to": "/messages"}, "Messages"), ) @component def all_messages(): - last_messages = { - ", ".join(msg["with"]): msg - for msg in sorted(message_data, key=lambda m: m["id"]) - } + last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])} + + messages = [] + for msg in last_messages.values(): + _link = link( + {"to": f"/messages/with/{'-'.join(msg['with'])}"}, + f"Conversation with: {', '.join(msg['with'])}", + ) + msg_from = f"{'' if msg['from'] is None else '🔴'} {msg['message']}" + messages.append(html.li({"key": msg["id"]}, html.p(_link), msg_from)) + return html.div( html.h1("All Messages đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else '🔴'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), + html.ul(messages), ) @component def messages_with(*names): - messages = [msg for msg in message_data if set(msg["with"]) == names] + messages = [msg for msg in message_data if tuple(msg["with"]) == names] return html.div( html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), html.ul( diff --git a/docs/examples/python/route-links.py b/docs/examples/python/route-links.py index f2be305..baf428c 100644 --- a/docs/examples/python/route-links.py +++ b/docs/examples/python/route-links.py @@ -1,14 +1,14 @@ from reactpy import component, html, run -from reactpy_router import link, route, simple +from reactpy_router import browser_router, link, route @component def root(): - return simple.router( + return browser_router( route("/", home()), route("/messages", html.h1("Messages đŸ’Ŧ")), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + route("{404:any}", html.h1("Missing Link 🔗‍đŸ’Ĩ")), ) @@ -16,7 +16,7 @@ def root(): def home(): return html.div( html.h1("Home Page 🏠"), - link("Messages", to="/messages"), + link({"to": "/messages"}, "Messages"), ) diff --git a/docs/examples/python/route-parameters.py b/docs/examples/python/route-parameters.py index 4fd30e2..a794742 100644 --- a/docs/examples/python/route-parameters.py +++ b/docs/examples/python/route-parameters.py @@ -1,8 +1,8 @@ from typing import TypedDict from reactpy import component, html, run -from reactpy_router import link, route, simple -from reactpy_router.core import use_params + +from reactpy_router import browser_router, link, route, use_params message_data: list["MessageDataType"] = [ {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, @@ -18,14 +18,14 @@ @component def root(): - return simple.router( + return browser_router( route("/", home()), route( "/messages", all_messages(), route("/with/{names}", messages_with()), # note the path param ), - route("*", html.h1("Missing Link 🔗‍đŸ’Ĩ")), + route("{404:any}", html.h1("Missing Link 🔗‍đŸ’Ĩ")), ) @@ -33,40 +33,32 @@ def root(): def home(): return html.div( html.h1("Home Page 🏠"), - link("Messages", to="/messages"), + link({"to": "/messages"}, "Messages"), ) @component def all_messages(): - last_messages = { - ", ".join(msg["with"]): msg - for msg in sorted(message_data, key=lambda m: m["id"]) - } + last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])} + messages = [] + for msg in last_messages.values(): + msg_hyperlink = link( + {"to": f"/messages/with/{'-'.join(msg['with'])}"}, + f"Conversation with: {', '.join(msg['with'])}", + ) + msg_from = f"{'' if msg['from'] is None else '🔴'} {msg['message']}" + messages.append(html.li({"key": msg["id"]}, html.p(msg_hyperlink), msg_from)) + return html.div( html.h1("All Messages đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else '🔴'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), + html.ul(messages), ) @component def messages_with(): - names = set(use_params()["names"].split("-")) # and here we use the path param - messages = [msg for msg in message_data if set(msg["with"]) == names] + names = tuple(use_params()["names"].split("-")) # and here we use the path param + messages = [msg for msg in message_data if tuple(msg["with"]) == names] return html.div( html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), html.ul( diff --git a/docs/examples/python/use-params.py b/docs/examples/python/use-params.py index 7b1193a..93a4f07 100644 --- a/docs/examples/python/use-params.py +++ b/docs/examples/python/use-params.py @@ -1,23 +1,26 @@ -from reactpy import component, html +from reactpy import component, html, run -from reactpy_router import link, route, simple, use_params +from reactpy_router import browser_router, link, route, use_params @component def user(): params = use_params() - return html.h1(f"User {params['id']} 👤") + return html._(html.h1(f"User {params['id']} 👤"), html.p("Nothing (yet).")) @component def root(): - return simple.router( + return browser_router( route( "/", html.div( html.h1("Home Page 🏠"), - link("User 123", to="/user/123"), + link({"to": "/user/123"}, "User 123"), ), ), route("/user/{id:int}", user()), ) + + +run(root) diff --git a/docs/examples/python/use-query.py b/docs/examples/python/use-query.py deleted file mode 100644 index a8678cc..0000000 --- a/docs/examples/python/use-query.py +++ /dev/null @@ -1,23 +0,0 @@ -from reactpy import component, html - -from reactpy_router import link, route, simple, use_query - - -@component -def search(): - query = use_query() - return html.h1(f"Search Results for {query['q'][0]} 🔍") - - -@component -def root(): - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link("Search", to="/search?q=reactpy"), - ), - ), - route("/about", html.h1("About Page 📖")), - ) diff --git a/docs/examples/python/use-search-params.py b/docs/examples/python/use-search-params.py new file mode 100644 index 0000000..faeba5e --- /dev/null +++ b/docs/examples/python/use-search-params.py @@ -0,0 +1,26 @@ +from reactpy import component, html, run + +from reactpy_router import browser_router, link, route, use_search_params + + +@component +def search(): + search_params = use_search_params() + return html._(html.h1(f"Search Results for {search_params['query'][0]} 🔍"), html.p("Nothing (yet).")) + + +@component +def root(): + return browser_router( + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link({"to": "/search?query=reactpy"}, "Search"), + ), + ), + route("/search", search()), + ) + + +run(root) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index d93b302..5173834 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,147 +1,137 @@ --- nav: - - Get Started: - - Add ReactPy-Router to Your Project: index.md - - Your First Routed Application: learn/simple-application.md - - Advanced Topics: - - Routers, Routes, and Links: learn/routers-routes-and-links.md - - Hooks: learn/hooks.md - - Creating a Custom Router 🚧: learn/custom-router.md - - Reference: - - Core: reference/core.md - - Router: reference/router.md - - Types: reference/types.md - - About: - - Changelog: about/changelog.md - - Contributor Guide: - - Code: about/code.md - - Docs: about/docs.md - - Community: - - GitHub Discussions: https://github.com/reactive-python/reactpy-router/discussions - - Discord: https://discord.gg/uNb5P4hA9X - - Reddit: https://www.reddit.com/r/ReactPy/ - - License: about/license.md + - Get Started: + - Add ReactPy-Router to Your Project: index.md + - Your First Routed Application: learn/your-first-app.md + - Advanced Topics: + - Routers, Routes, and Links: learn/routers-routes-and-links.md + - Hooks: learn/hooks.md + - Creating a Custom Router 🚧: learn/custom-router.md + - Reference: + - Router Components: reference/router.md + - Components: reference/components.md + - Hooks: reference/hooks.md + - Types: reference/types.md + - About: + - Changelog: about/changelog.md + - Contributor Guide: + - Code: about/code.md + - Docs: about/docs.md + - Community: + - GitHub Discussions: https://github.com/reactive-python/reactpy-router/discussions + - Discord: https://discord.gg/uNb5P4hA9X + - Reddit: https://www.reddit.com/r/ReactPy/ + - License: about/license.md theme: - name: material - custom_dir: overrides - palette: - - media: "(prefers-color-scheme: dark)" - scheme: slate - toggle: - icon: material/white-balance-sunny - name: Switch to light mode - primary: red # We use red to indicate that something is unthemed - accent: red - - media: "(prefers-color-scheme: light)" - scheme: default - toggle: - icon: material/weather-night - name: Switch to dark mode - primary: white - accent: red - features: - - navigation.instant - - navigation.tabs - - navigation.tabs.sticky - - navigation.top - - content.code.copy - - search.highlight - icon: - repo: fontawesome/brands/github - admonition: - note: fontawesome/solid/note-sticky - logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg - favicon: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg + name: material + custom_dir: overrides + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/white-balance-sunny + name: Switch to light mode + primary: red # We use red to indicate that something is unthemed + accent: red + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-night + name: Switch to dark mode + primary: white + accent: red + features: + - navigation.instant + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - content.code.copy + - search.highlight + icon: + repo: fontawesome/brands/github + admonition: + note: fontawesome/solid/note-sticky + logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg + favicon: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg markdown_extensions: - - toc: - permalink: true - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - pymdownx.tabbed: - alternate_style: true - - pymdownx.highlight: - linenums: true - - pymdownx.superfences - - pymdownx.details - - pymdownx.inlinehilite - - admonition - - attr_list - - md_in_html - - pymdownx.keys + - toc: + permalink: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + linenums: true + - pymdownx.superfences + - pymdownx.details + - pymdownx.inlinehilite + - admonition + - attr_list + - md_in_html + - pymdownx.keys plugins: - - search - - include-markdown - - git-authors - - minify: - minify_html: true - minify_js: true - minify_css: true - cache_safe: true - - git-revision-date-localized: - fallback_to_build_date: true - - spellcheck: - known_words: dictionary.txt - allow_unicode: no - ignore_code: yes - skip_files: - - "index.md" - - "reference\\core.md" - - "reference/core.md" - - "reference\\types.md" - - "reference/types.md" - - mkdocstrings: - default_handler: python - handlers: - python: - paths: ["../"] - import: - - https://reactpy.dev/docs/objects.inv - - https://installer.readthedocs.io/en/stable/objects.inv - + - search + - include-markdown + - git-authors + - minify: + minify_html: true + minify_js: true + minify_css: true + cache_safe: true + - git-revision-date-localized: + fallback_to_build_date: true + - spellcheck: + known_words: dictionary.txt + allow_unicode: no + - mkdocstrings: + default_handler: python + handlers: + python: + paths: ["../"] + import: + - https://reactpy.dev/docs/objects.inv + - https://installer.readthedocs.io/en/stable/objects.inv + options: + show_bases: false + show_root_members_full_path: true extra: - generator: false - version: - provider: mike - analytics: - provider: google - property: G-XRLQYZBG00 + generator: false + version: + provider: mike + analytics: + provider: google + property: G-XRLQYZBG00 extra_javascript: - - assets/js/main.js + - assets/js/main.js extra_css: - - assets/css/main.css - - assets/css/button.css - - assets/css/admonition.css - - assets/css/banner.css - - assets/css/sidebar.css - - assets/css/navbar.css - - assets/css/table-of-contents.css - - assets/css/code.css - - assets/css/footer.css - - assets/css/home.css + - assets/css/main.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/banner.css + - assets/css/sidebar.css + - assets/css/navbar.css + - assets/css/table-of-contents.css + - assets/css/code.css + - assets/css/footer.css + - assets/css/home.css watch: - - "../docs" - - ../README.md - - ../CHANGELOG.md - - ../LICENSE.md - - "../src" + - "../docs" + - ../README.md + - ../CHANGELOG.md + - ../LICENSE.md + - "../src" site_name: ReactPy Router site_author: Archmonger site_description: It's React-Router, but in Python. -copyright: '© -
- -Reactive Python and affiliates. -' +copyright: '©
Reactive Python and affiliates.' repo_url: https://github.com/reactive-python/reactpy-router site_url: https://reactive-python.github.io/reactpy-router repo_name: ReactPy Router (GitHub) diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/home-code-examples/add-interactivity-demo.html deleted file mode 100644 index 48ac19a..0000000 --- a/docs/overrides/home-code-examples/add-interactivity-demo.html +++ /dev/null @@ -1,172 +0,0 @@ -
-
- -
-
- - - - example.com/videos.html -
-
- -
-
-

Searchable Videos

-

Type a search query below.

- -
- -

5 Videos

- -
-
- - - -
-
-

ReactPy: The Documentary

-

From web library to taco delivery service

-
- -
- -
-
- - - -
-
-

Code using Worst Practices

-

Harriet Potter (2013)

-
- -
- -
-
- - - -
-
-

Introducing ReactPy Foriegn

-

Tim Cooker (2015)

-
- -
- -
-
- - - -
-
-

Introducing ReactPy Cooks

-

Soap Boat and Dinosaur Dan (2018)

-
- -
- -
-
- - - -
-
-

Introducing Quantum Components

-

Isaac Asimov and Lauren-kun (2020)

-
- -
-

-
- - -
-
diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/home-code-examples/add-interactivity.py deleted file mode 100644 index 9097644..0000000 --- a/docs/overrides/home-code-examples/add-interactivity.py +++ /dev/null @@ -1,30 +0,0 @@ -from reactpy import component, html, use_state - - -def filter_videos(videos, search_text): - return None - - -def search_input(dictionary, value): - return None - - -def video_list(videos, empty_heading): - return None - - -@component -def searchable_video_list(videos): - search_text, set_search_text = use_state("") - found_videos = filter_videos(videos, search_text) - - return html._( - search_input( - {"on_change": lambda new_text: set_search_text(new_text)}, - value=search_text, - ), - video_list( - videos=found_videos, - empty_heading=f"No matches for “{search_text}”", - ), - ) diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/home-code-examples/code-block.html deleted file mode 100644 index c1f14e5..0000000 --- a/docs/overrides/home-code-examples/code-block.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
-
- -
-
diff --git a/docs/overrides/home-code-examples/create-user-interfaces-demo.html b/docs/overrides/home-code-examples/create-user-interfaces-demo.html deleted file mode 100644 index 9a684d3..0000000 --- a/docs/overrides/home-code-examples/create-user-interfaces-demo.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
-
-
- - - -
-
-

My video

-

Video description

-
- -
-
-
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 37776ab..0000000 --- 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-code-examples/write-components-with-python-demo.html b/docs/overrides/home-code-examples/write-components-with-python-demo.html deleted file mode 100644 index 203287c..0000000 --- a/docs/overrides/home-code-examples/write-components-with-python-demo.html +++ /dev/null @@ -1,65 +0,0 @@ -
-
-

3 Videos

-
-
- - - -
-
-

First video

-

Video description

-
- -
-
-
- - - -
-
-

Second video

-

Video description

-
- -
-
-
- - - -
-
-

Third video

-

Video description

-
- -
-
-
diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/home-code-examples/write-components-with-python.py deleted file mode 100644 index 6af43ba..0000000 --- a/docs/overrides/home-code-examples/write-components-with-python.py +++ /dev/null @@ -1,15 +0,0 @@ -from reactpy import component, html - - -@component -def video_list(videos, empty_heading): - count = len(videos) - heading = empty_heading - if count > 0: - noun = "Videos" if count > 1 else "Video" - heading = f"{count} {noun}" - - return html.section( - html.h2(heading), - [video(video) for video in videos], - ) diff --git a/docs/src/about/code.md b/docs/src/about/code.md index 5d73042..0eda9ee 100644 --- a/docs/src/about/code.md +++ b/docs/src/about/code.md @@ -40,12 +40,10 @@ By running the command below you can run the full test suite: nox -t test ``` -Or, if you want to run the tests in the foreground with a visible browser window, run: - - +Or, if you want to run the tests in the background run: ```bash linenums="0" -nox -t test -- --headed +nox -t test -- --headless ``` ## Creating a pull request diff --git a/docs/src/learn/hooks.md b/docs/src/learn/hooks.md index 3479ffc..7ed3821 100644 --- a/docs/src/learn/hooks.md +++ b/docs/src/learn/hooks.md @@ -6,19 +6,19 @@ Several pre-fabricated hooks are provided to help integrate with routing feature --- -## Use Query +## Use Search Parameters -The [`use_query`][src.reactpy_router.use_query] hook can be used to access query parameters from the current location. It returns a dictionary of query parameters, where each value is a list of strings. +The [`use_search_params`][reactpy_router.use_search_params] hook can be used to access query parameters from the current location. It returns a dictionary of query parameters, where each value is a list of strings. === "components.py" ```python - {% include "../../examples/python/use-query.py" %} + {% include "../../examples/python/use-search-params.py" %} ``` ## Use Parameters -The [`use_params`][src.reactpy_router.use_params] hook can be used to access route parameters from the current location. It returns a dictionary of route parameters, where each value is mapped to a value that matches the type specified in the route path. +The [`use_params`][reactpy_router.use_params] hook can be used to access route parameters from the current location. It returns a dictionary of route parameters, where each value is mapped to a value that matches the type specified in the route path. === "components.py" diff --git a/docs/src/learn/routers-routes-and-links.md b/docs/src/learn/routers-routes-and-links.md index af62578..f0abfc1 100644 --- a/docs/src/learn/routers-routes-and-links.md +++ b/docs/src/learn/routers-routes-and-links.md @@ -4,14 +4,14 @@ We include built-in components that automatically handle routing, which enable S ## Routers and Routes -The [`simple.router`][src.reactpy_router.simple.router] component is one possible implementation of a [Router][src.reactpy_router.types.Router]. Routers takes a series of [route][src.reactpy_router.route] objects as positional arguments and render whatever element matches the current location. +The [`browser_router`][reactpy_router.browser_router] component is one possible implementation of a [Router][reactpy_router.types.Router]. Routers takes a series of [route][reactpy_router.route] objects as positional arguments and render whatever element matches the current location. !!! abstract "Note" The current location is determined based on the browser's current URL and can be found by checking the [`use_location`][reactpy.backend.hooks.use_location] hook. -Here's a basic example showing how to use `#!python simple.router` with two routes. +Here's a basic example showing how to use `#!python browser_router` with two routes. === "components.py" @@ -19,11 +19,11 @@ Here's a basic example showing how to use `#!python simple.router` with two rout {% include "../../examples/python/basic-routing.py" %} ``` -Here we'll note some special syntax in the route path for the second route. The `#!python "*"` is a wildcard that will match any path. This is useful for creating a "404" page that will be shown when no other route matches. +Here we'll note some special syntax in the route path for the second route. The `#!python "any"` type is a wildcard that will match any path. This is useful for creating a default page or error page such as "404 NOT FOUND". -### Simple Router +### Browser Router -The syntax for declaring routes with the [simple.router][src.reactpy_router.simple.router] is very similar to the syntax used by [`starlette`](https://www.starlette.io/routing/) (a popular Python web framework). As such route parameters are declared using the following syntax: +The syntax for declaring routes with the [`browser_router`][reactpy_router.browser_router] is very similar to the syntax used by [`starlette`](https://www.starlette.io/routing/) (a popular Python web framework). As such route parameters are declared using the following syntax: ```python linenums="0" /my/route/{param} @@ -38,7 +38,9 @@ In this case, `#!python param` is the name of the route parameter and the option | `#!python int` | `#!python \d+` | | `#!python float` | `#!python \d+(\.\d+)?` | | `#!python uuid` | `#!python [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` | +| `#!python slug` | `#!python [-a-zA-Z0-9_]+` | | `#!python path` | `#!python .+` | +| `#!python any` | `#!python .*` | So in practice these each might look like: @@ -50,15 +52,15 @@ So in practice these each might look like: /my/route/{param:path} ``` -Any route parameters collected from the current location then be accessed using the [`use_params`](#using-parameters) hook. +Any route parameters collected from the current location then be accessed using the [`use_params`](hooks.md#use-parameters) hook. !!! warning "Pitfall" - While it is possible to use route parameters to capture values from query strings (such as `#!python /my/route/?foo={bar}`), this is not recommended. Instead, you should use the [`use_query`][src.reactpy_router.use_query] hook to access query string values. + While it is possible to use route parameters to capture values from query strings (such as `#!python /my/route/?foo={bar}`), this is not recommended. Instead, you should use the [`use_search_params`][reactpy_router.use_search_params] hook to access query string values. ## Route Links -Links between routes should be created using the [link][src.reactpy_router.link] component. This will allow ReactPy to handle the transition between routes and avoid a page reload. +Links between routes should be created using the [link][reactpy_router.link] component. This will allow ReactPy to handle the transition between routes and avoid a page reload. === "components.py" diff --git a/docs/src/learn/simple-application.md b/docs/src/learn/your-first-app.md similarity index 93% rename from docs/src/learn/simple-application.md rename to docs/src/learn/your-first-app.md index 8f2a5b5..4b67677 100644 --- a/docs/src/learn/simple-application.md +++ b/docs/src/learn/your-first-app.md @@ -1,6 +1,6 @@

-Here you'll learn the various features of `reactpy-router` and how to use them. These examples will utilize the [`reactpy_router.simple.router`][src.reactpy_router.simple.router]. +Here you'll learn the various features of `reactpy-router` and how to use them. These examples will utilize the [`reactpy_router.browser_router`][reactpy_router.browser_router].

@@ -39,7 +39,7 @@ The first step is to create a basic router that will display the home page when {% include "../../examples/python/basic-routing.py" %} ``` -When navigating to [`http://127.0.0.1:8000``](http://127.0.0.1:8000) you should see `Home Page 🏠`. However, if you go to any other route you will instead see `Missing Link 🔗‍đŸ’Ĩ`. +When navigating to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) you should see `Home Page 🏠`. However, if you go to any other route you will instead see `Missing Link 🔗‍đŸ’Ĩ`. With this foundation you can start adding more routes. diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md new file mode 100644 index 0000000..f1cc570 --- /dev/null +++ b/docs/src/reference/components.md @@ -0,0 +1,4 @@ +::: reactpy_router + + options: + members: ["route", "link"] diff --git a/docs/src/reference/core.md b/docs/src/reference/core.md deleted file mode 100644 index 26cf9e5..0000000 --- a/docs/src/reference/core.md +++ /dev/null @@ -1 +0,0 @@ -::: src.reactpy_router.core diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md new file mode 100644 index 0000000..d3cfa18 --- /dev/null +++ b/docs/src/reference/hooks.md @@ -0,0 +1,4 @@ +::: reactpy_router + + options: + members: ["use_params", "use_search_params"] diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md index 2fcea59..5700cf5 100644 --- a/docs/src/reference/router.md +++ b/docs/src/reference/router.md @@ -1 +1,4 @@ -::: src.reactpy_router.simple +::: reactpy_router + + options: + members: ["browser_router"] diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index 0482432..204bee7 100644 --- a/docs/src/reference/types.md +++ b/docs/src/reference/types.md @@ -1 +1 @@ -::: src.reactpy_router.types +::: reactpy_router.types diff --git a/noxfile.py b/noxfile.py index 1eebea2..6376072 100644 --- a/noxfile.py +++ b/noxfile.py @@ -33,7 +33,6 @@ def test_types(session: Session) -> None: @session(tags=["test"]) def test_style(session: Session) -> None: install_requirements_file(session, "check-style") - session.run("black", ".", "--check") session.run("ruff", "check", ".") diff --git a/pyproject.toml b/pyproject.toml index 763f3a0..d6a0110 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,9 @@ warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true -[tool.ruff.isort] -known-first-party = ["src", "tests"] - [tool.ruff] -ignore = ["E501"] +lint.ignore = ["E501"] +lint.isort.known-first-party = ["src", "tests"] extend-exclude = [".venv/*", ".eggs/*", ".nox/*", "build/*"] line-length = 120 diff --git a/requirements/check-style.txt b/requirements/check-style.txt index e4f6562..af3ee57 100644 --- a/requirements/check-style.txt +++ b/requirements/check-style.txt @@ -1,2 +1 @@ -black >=23,<24 ruff diff --git a/src/js/package-lock.json b/src/js/package-lock.json index db77c4d..c98d3c6 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -6,7 +6,6 @@ "": { "name": "reactpy-router", "dependencies": { - "htm": "^3.0.4", "react": "^17.0.1", "react-dom": "^17.0.1" }, @@ -1098,11 +1097,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/htm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", - "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3126,11 +3120,6 @@ "has-symbols": "^1.0.2" } }, - "htm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", - "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" - }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", diff --git a/src/js/package.json b/src/js/package.json index 9baef8c..d4ac92c 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -18,16 +18,15 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "devDependencies": { - "prettier": "^2.2.1", "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", + "prettier": "^2.2.1", "rollup": "^2.35.1", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0" }, "dependencies": { - "htm": "^3.0.4", "react": "^17.0.1", "react-dom": "^17.0.1" } diff --git a/src/js/rollup.config.js b/src/js/rollup.config.js index ab1d0b1..396fd87 100644 --- a/src/js/rollup.config.js +++ b/src/js/rollup.config.js @@ -5,7 +5,7 @@ import replace from "rollup-plugin-replace"; export default { input: "src/index.js", output: { - file: "../reactpy_router/bundle.js", + file: "../reactpy_router/static/bundle.js", format: "esm", }, plugins: [ diff --git a/src/js/src/index.js b/src/js/src/index.js index 1f43092..8ead7eb 100644 --- a/src/js/src/index.js +++ b/src/js/src/index.js @@ -1,8 +1,5 @@ import React from "react"; import ReactDOM from "react-dom"; -import htm from "htm"; - -const html = htm.bind(React.createElement); export function bind(node) { return { @@ -15,30 +12,69 @@ export function bind(node) { }; } -export function History({ onChange }) { - // capture changes to the browser's history +export function History({ onHistoryChange }) { + // Capture browser "history go back" action and tell the server about it + // Note: Browsers do not allow us to detect "history go forward" actions. React.useEffect(() => { + // Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback. const listener = () => { - onChange({ + onHistoryChange({ pathname: window.location.pathname, search: window.location.search, }); }; + + // Register the event listener window.addEventListener("popstate", listener); + + // Delete the event listener when the component is unmounted return () => window.removeEventListener("popstate", listener); }); - return null; -} -export function Link({ to, onClick, children, ...props }) { - const handleClick = (event) => { - event.preventDefault(); - window.history.pushState({}, to, new URL(to, window.location)); - onClick({ + // Tell the server about the URL during the initial page load + // FIXME: This currently runs every time any component is mounted due to a ReactPy core rendering bug. + // https://github.com/reactive-python/reactpy/pull/1224 + React.useEffect(() => { + onHistoryChange({ pathname: window.location.pathname, search: window.location.search, }); - }; + return () => {}; + }, []); + return null; +} - return html`
${children}`; +// FIXME: The Link component is unused due to a ReactPy core rendering bug +// which causes duplicate rendering (and thus duplicate event listeners). +// https://github.com/reactive-python/reactpy/pull/1224 +export function Link({ onClick, linkClass }) { + // This component is not the actual anchor link. + // It is an event listener for the link component created by ReactPy. + React.useEffect(() => { + // Event function that will tell the server about clicks + const handleClick = (event) => { + event.preventDefault(); + let to = event.target.getAttribute("href"); + window.history.pushState({}, to, new URL(to, window.location)); + onClick({ + pathname: window.location.pathname, + search: window.location.search, + }); + }; + + // Register the event listener + let link = document.querySelector(`.${linkClass}`); + if (link) { + link.addEventListener("click", handleClick); + } + + // Delete the event listener when the component is unmounted + return () => { + let link = document.querySelector(`.${linkClass}`); + if (link) { + link.removeEventListener("click", handleClick); + } + }; + }); + return null; } diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index cb2fcbc..fa2781f 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -1,20 +1,16 @@ # the version is statically loaded by setup.py __version__ = "0.1.1" -from . import simple -from .core import create_router, link, route, router_component, use_params, use_query -from .types import Route, RouteCompiler, RouteResolver + +from .components import link, route +from .hooks import use_params, use_search_params +from .routers import browser_router, create_router __all__ = ( "create_router", "link", "route", - "route", - "Route", - "RouteCompiler", - "router_component", - "RouteResolver", - "simple", + "browser_router", "use_params", - "use_query", + "use_search_params", ) diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py new file mode 100644 index 0000000..3008065 --- /dev/null +++ b/src/reactpy_router/components.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any +from urllib.parse import urljoin +from uuid import uuid4 + +from reactpy import component, event, html, use_connection +from reactpy.backend.types import Location +from reactpy.core.component import Component +from reactpy.core.types import VdomDict +from reactpy.web.module import export, module_from_file + +from reactpy_router.hooks import _use_route_state +from reactpy_router.types import Route + +History = export( + module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), + ("History"), +) +"""Client-side portion of history handling""" + +Link = export( + module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), + ("Link"), +) +"""Client-side portion of link handling""" + +link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8") + + +def link(attributes: dict[str, Any], *children: Any) -> Component: + """Create a link with the given attributes and children.""" + return _link(attributes, *children) + + +@component +def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: + """A component that renders a link to the given path.""" + attributes = attributes.copy() + uuid_string = f"link-{uuid4().hex}" + class_name = f"{uuid_string}" + set_location = _use_route_state().set_location + if "className" in attributes: + class_name = " ".join([attributes.pop("className"), class_name]) + if "class_name" in attributes: # pragma: no cover + # TODO: This can be removed when ReactPy stops supporting underscores in attribute names + class_name = " ".join([attributes.pop("class_name"), class_name]) + if "href" in attributes and "to" not in attributes: + attributes["to"] = attributes.pop("href") + if "to" not in attributes: # pragma: no cover + raise ValueError("The `to` attribute is required for the `Link` component.") + to = attributes.pop("to") + + attrs = { + **attributes, + "href": to, + "className": class_name, + } + + # FIXME: This component currently works in a "dumb" way by trusting that ReactPy's script tag \ + # properly sets the location due to bugs in ReactPy rendering. + # https://github.com/reactive-python/reactpy/pull/1224 + current_path = use_connection().location.pathname + + @event(prevent_default=True) + def on_click(_event: dict[str, Any]) -> None: + pathname, search = to.split("?", 1) if "?" in to else (to, "") + if search: + search = f"?{search}" + + # Resolve relative paths that match `../foo` + if pathname.startswith("../"): + pathname = urljoin(current_path, pathname) + + # Resolve relative paths that match `foo` + if not pathname.startswith("/"): + pathname = urljoin(current_path, pathname) + + # Resolve relative paths that match `/foo/../bar` + while "/../" in pathname: + part_1, part_2 = pathname.split("/../", 1) + pathname = urljoin(f"{part_1}/", f"../{part_2}") + + # Resolve relative paths that match `foo/./bar` + pathname = pathname.replace("/./", "/") + + set_location(Location(pathname, search)) + + attrs["onClick"] = on_click + + return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string))) + + # def on_click(_event: dict[str, Any]) -> None: + # set_location(Location(**_event)) + # return html._(html.a(attrs, *children), Link({"onClick": on_click, "linkClass": uuid_string})) + + +def route(path: str, element: Any | None, *routes: Route) -> Route: + """Create a route with the given path, element, and child routes.""" + return Route(path, element, routes) diff --git a/src/reactpy_router/converters.py b/src/reactpy_router/converters.py new file mode 100644 index 0000000..5fe1b5e --- /dev/null +++ b/src/reactpy_router/converters.py @@ -0,0 +1,37 @@ +import uuid + +from reactpy_router.types import ConversionInfo + +__all__ = ["CONVERTERS"] + +CONVERTERS: dict[str, ConversionInfo] = { + "int": { + "regex": r"\d+", + "func": int, + }, + "str": { + "regex": r"[^/]+", + "func": str, + }, + "uuid": { + "regex": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "func": uuid.UUID, + }, + "slug": { + "regex": r"[-a-zA-Z0-9_]+", + "func": str, + }, + "path": { + "regex": r".+", + "func": str, + }, + "float": { + "regex": r"\d+(\.\d+)?", + "func": float, + }, + "any": { + "regex": r".*", + "func": str, + }, +} +"""The conversion types supported by the default Resolver.""" diff --git a/src/reactpy_router/core.py b/src/reactpy_router/core.py deleted file mode 100644 index 490a78c..0000000 --- a/src/reactpy_router/core.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Core functionality for the reactpy-router package.""" - -from __future__ import annotations - -from dataclasses import dataclass, replace -from pathlib import Path -from typing import Any, Callable, Iterator, Sequence, TypeVar -from urllib.parse import parse_qs - -from reactpy import ( - component, - create_context, - html, - use_context, - use_location, - use_memo, - use_state, -) -from reactpy.backend.hooks import ConnectionContext, use_connection -from reactpy.backend.types import Connection, Location -from reactpy.core.types import VdomChild, VdomDict -from reactpy.types import ComponentType, Context -from reactpy.web.module import export, module_from_file - -from reactpy_router.types import Route, RouteCompiler, Router, RouteResolver - -R = TypeVar("R", bound=Route) - - -def route(path: str, element: Any | None, *routes: Route) -> Route: - """Create a route with the given path, element, and child routes""" - return Route(path, element, routes) - - -def create_router(compiler: RouteCompiler[R]) -> Router[R]: - """A decorator that turns a route compiler into a router""" - - def wrapper(*routes: R) -> ComponentType: - return router_component(*routes, compiler=compiler) - - return wrapper - - -@component -def router_component( - *routes: R, - compiler: RouteCompiler[R], -) -> VdomDict | None: - """A component that renders the first matching route using the given compiler""" - - old_conn = use_connection() - location, set_location = use_state(old_conn.location) - - resolvers = use_memo( - lambda: tuple(map(compiler, _iter_routes(routes))), - dependencies=(compiler, hash(routes)), - ) - - match = use_memo(lambda: _match_route(resolvers, location)) - - if match is not None: - element, params = match - return html._( - ConnectionContext( - _route_state_context(element, value=_RouteState(set_location, params)), - value=Connection(old_conn.scope, location, old_conn.carrier), - ), - _history({"on_change": lambda event: set_location(Location(**event))}), - ) - - return None - - -@component -def link(*children: VdomChild, to: str, **attributes: Any) -> VdomDict: - """A component that renders a link to the given path""" - set_location = _use_route_state().set_location - attrs = { - **attributes, - "to": to, - "onClick": lambda event: set_location(Location(**event)), - } - return _link(attrs, *children) - - -def use_params() -> dict[str, Any]: - """Get parameters from the currently matching route pattern""" - return _use_route_state().params - - -def use_query( - keep_blank_values: bool = False, - strict_parsing: bool = False, - errors: str = "replace", - max_num_fields: int | None = None, - separator: str = "&", -) -> dict[str, list[str]]: - """See :func:`urllib.parse.parse_qs` for parameter info.""" - return parse_qs( - use_location().search[1:], - keep_blank_values=keep_blank_values, - strict_parsing=strict_parsing, - errors=errors, - max_num_fields=max_num_fields, - separator=separator, - ) - - -def _iter_routes(routes: Sequence[R]) -> Iterator[R]: - for parent in routes: - for child in _iter_routes(parent.routes): - yield replace(child, path=parent.path + child.path) # type: ignore[misc] - yield parent - - -def _match_route( - compiled_routes: Sequence[RouteResolver], location: Location -) -> tuple[Any, dict[str, Any]] | None: - for resolver in compiled_routes: - match = resolver.resolve(location.pathname) - if match is not None: - return match - return None - - -_link, _history = export( - module_from_file("reactpy-router", file=Path(__file__).parent / "bundle.js"), - ("Link", "History"), -) - - -@dataclass -class _RouteState: - set_location: Callable[[Location], None] - params: dict[str, Any] - - -def _use_route_state() -> _RouteState: - route_state = use_context(_route_state_context) - assert route_state is not None - return route_state - - -_route_state_context: Context[_RouteState | None] = create_context(None) diff --git a/src/reactpy_router/hooks.py b/src/reactpy_router/hooks.py new file mode 100644 index 0000000..3831acf --- /dev/null +++ b/src/reactpy_router/hooks.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable +from urllib.parse import parse_qs + +from reactpy import create_context, use_context, use_location +from reactpy.backend.types import Location +from reactpy.types import Context + + +@dataclass +class _RouteState: + set_location: Callable[[Location], None] + params: dict[str, Any] + + +def _use_route_state() -> _RouteState: + route_state = use_context(_route_state_context) + if route_state is None: # pragma: no cover + raise RuntimeError( + "ReactPy-Router was unable to find a route context. Are you " + "sure this hook/component is being called within a router?" + ) + + return route_state + + +_route_state_context: Context[_RouteState | None] = create_context(None) + + +def use_params() -> dict[str, Any]: + """The `use_params` hook returns an object of key/value pairs of the dynamic parameters \ + from the current URL that were matched by the `Route`. Child routes inherit all parameters \ + from their parent routes. + + For example, if you have a `URL_PARAM` defined in the route `/example//`, + this hook will return the URL_PARAM value that was matched.""" + + # TODO: Check if this returns all parent params + return _use_route_state().params + + +def use_search_params( + keep_blank_values: bool = False, + strict_parsing: bool = False, + errors: str = "replace", + max_num_fields: int | None = None, + separator: str = "&", +) -> dict[str, list[str]]: + """ + The `use_search_params` hook is used to read the query string in the URL \ + for the current location. + + See `urllib.parse.parse_qs` for info on this hook's parameters.""" + location = use_location() + query_string = location.search[1:] if len(location.search) > 1 else "" + + # TODO: In order to match `react-router`, this will need to return a tuple of the search params \ + # and a function to update them. This is currently not possible without reactpy core having a \ + # communication layer. + return parse_qs( + query_string, + keep_blank_values=keep_blank_values, + strict_parsing=strict_parsing, + errors=errors, + max_num_fields=max_num_fields, + separator=separator, + ) diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py new file mode 100644 index 0000000..55c6a01 --- /dev/null +++ b/src/reactpy_router/resolvers.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import re +from typing import Any + +from reactpy_router.converters import CONVERTERS +from reactpy_router.types import ConversionInfo, ConverterMapping, Route + +__all__ = ["StarletteResolver"] + + +class StarletteResolver: + """URL resolver that matches routes using starlette's URL routing syntax. + + However, this resolver adds a few additional parameter types on top of Starlette's syntax.""" + + def __init__( + self, + route: Route, + param_pattern=r"{(?P\w+)(?P:\w+)?}", + converters: dict[str, ConversionInfo] | None = None, + ) -> None: + self.element = route.element + self.registered_converters = converters or CONVERTERS + self.converter_mapping: ConverterMapping = {} + self.param_regex = re.compile(param_pattern) + self.pattern = self.parse_path(route.path) + self.key = self.pattern.pattern # Unique identifier for ReactPy rendering + + def parse_path(self, path: str) -> re.Pattern[str]: + # Convert path to regex pattern, then interpret using registered converters + pattern = "^" + last_match_end = 0 + + # Iterate through matches of the parameter pattern + for match in self.param_regex.finditer(path): + # Extract parameter name + name = match.group("name") + if name[0].isnumeric(): + # Regex group names can't begin with a number, so we must prefix them with + # "_numeric_". This prefix is removed later within this function. + name = f"_numeric_{name}" + + # Extract the parameter type + param_type = (match.group("type") or "str").strip(":") + + # Check if a converter exists for the type + try: + conversion_info = self.registered_converters[param_type] + except KeyError as e: + raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}") from e + + # Add the string before the match to the pattern + pattern += re.escape(path[last_match_end : match.start()]) + + # Add the match to the pattern + pattern += f"(?P<{name}>{conversion_info['regex']})" + + # Keep a local mapping of the URL's parameter names to conversion functions. + self.converter_mapping[name] = conversion_info["func"] + + # Update the last match end + last_match_end = match.end() + + # Add the string after the last match + pattern += f"{re.escape(path[last_match_end:])}$" + + return re.compile(pattern) + + def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: + match = self.pattern.match(path) + if match: + # Convert the matched groups to the correct types + params = { + parameter_name[len("_numeric_") :] + if parameter_name.startswith("_numeric_") + else parameter_name: self.converter_mapping[parameter_name](value) + for parameter_name, value in match.groupdict().items() + } + return (self.element, params) + return None diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py new file mode 100644 index 0000000..25b72c1 --- /dev/null +++ b/src/reactpy_router/routers.py @@ -0,0 +1,110 @@ +"""URL router implementation for ReactPy""" + +from __future__ import annotations + +from dataclasses import replace +from logging import getLogger +from typing import Any, Iterator, Literal, Sequence + +from reactpy import component, use_memo, use_state +from reactpy.backend.hooks import ConnectionContext, use_connection +from reactpy.backend.types import Connection, Location +from reactpy.core.types import VdomDict +from reactpy.types import ComponentType + +from reactpy_router.components import History +from reactpy_router.hooks import _route_state_context, _RouteState +from reactpy_router.resolvers import StarletteResolver +from reactpy_router.types import CompiledRoute, Resolver, Router, RouteType + +__all__ = ["browser_router", "create_router"] +_logger = getLogger(__name__) + + +def create_router(resolver: Resolver[RouteType]) -> Router[RouteType]: + """A decorator that turns a resolver into a router""" + + def wrapper(*routes: RouteType) -> ComponentType: + return router(*routes, resolver=resolver) + + return wrapper + + +browser_router = create_router(StarletteResolver) +"""This is the recommended router for all ReactPy Router web projects. +It uses the JavaScript DOM History API to manage the history stack.""" + + +@component +def router( + *routes: RouteType, + resolver: Resolver[RouteType], +) -> VdomDict | None: + """A component that renders matching route(s) using the given resolver. + + This typically should never be used by a user. Instead, use `create_router` if creating + a custom routing engine.""" + + old_conn = use_connection() + location, set_location = use_state(old_conn.location) + + resolvers = use_memo( + lambda: tuple(map(resolver, _iter_routes(routes))), + dependencies=(resolver, hash(routes)), + ) + + match = use_memo(lambda: _match_route(resolvers, location, select="first")) + + if match: + route_elements = [ + _route_state_context( + element, + value=_RouteState(set_location, params), + ) + for element, params in match + ] + + def on_history_change(event: dict[str, Any]) -> None: + """Callback function used within the JavaScript `History` component.""" + new_location = Location(**event) + if location != new_location: + set_location(new_location) + + return ConnectionContext( + History({"onHistoryChange": on_history_change}), # type: ignore[return-value] + *route_elements, + value=Connection(old_conn.scope, location, old_conn.carrier), + ) + + return None + + +def _iter_routes(routes: Sequence[RouteType]) -> Iterator[RouteType]: + for parent in routes: + for child in _iter_routes(parent.routes): + yield replace(child, path=parent.path + child.path) # type: ignore[misc] + yield parent + + +def _match_route( + compiled_routes: Sequence[CompiledRoute], + location: Location, + select: Literal["first", "all"], +) -> list[tuple[Any, dict[str, Any]]]: + matches = [] + + for resolver in compiled_routes: + match = resolver.resolve(location.pathname) + if match is not None: + if select == "first": + return [match] + + # Matching multiple routes is disabled since `react-router` no longer supports multiple + # matches via the `Route` component. However, it's kept here to support future changes + # or third-party routers. + matches.append(match) # pragma: no cover + + if not matches: + _logger.debug("No matching route found for %s", location.pathname) + + return matches diff --git a/src/reactpy_router/simple.py b/src/reactpy_router/simple.py deleted file mode 100644 index 256f78d..0000000 --- a/src/reactpy_router/simple.py +++ /dev/null @@ -1,98 +0,0 @@ -"""A simple router implementation for ReactPy""" - -from __future__ import annotations - -import re -import uuid -from typing import Any, Callable - -from typing_extensions import TypeAlias, TypedDict - -from reactpy_router.core import create_router -from reactpy_router.types import Route - -__all__ = ["router"] - -ConversionFunc: TypeAlias = "Callable[[str], Any]" -ConverterMapping: TypeAlias = "dict[str, ConversionFunc]" - -STAR_PATTERN = re.compile("^.*$") -PARAM_PATTERN = re.compile(r"{(?P\w+)(?P:\w+)?}") - - -class SimpleResolver: - """A simple route resolver that uses regex to match paths""" - - def __init__(self, route: Route) -> None: - self.element = route.element - self.pattern, self.converters = parse_path(route.path) - self.key = self.pattern.pattern - - def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: - match = self.pattern.match(path) - if match: - return ( - self.element, - {k: self.converters[k](v) for k, v in match.groupdict().items()}, - ) - return None - - -def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]: - if path == "*": - return STAR_PATTERN, {} - - pattern = "^" - last_match_end = 0 - converters: ConverterMapping = {} - for match in PARAM_PATTERN.finditer(path): - param_name = match.group("name") - param_type = (match.group("type") or "str").lstrip(":") - try: - param_conv = CONVERSION_TYPES[param_type] - except KeyError: - raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}") - pattern += re.escape(path[last_match_end : match.start()]) - pattern += f"(?P<{param_name}>{param_conv['regex']})" - converters[param_name] = param_conv["func"] - last_match_end = match.end() - pattern += re.escape(path[last_match_end:]) + "$" - return re.compile(pattern), converters - - -class ConversionInfo(TypedDict): - """Information about a conversion type""" - - regex: str - """The regex to match the conversion type""" - func: ConversionFunc - """The function to convert the matched string to the expected type""" - - -CONVERSION_TYPES: dict[str, ConversionInfo] = { - "str": { - "regex": r"[^/]+", - "func": str, - }, - "int": { - "regex": r"\d+", - "func": int, - }, - "float": { - "regex": r"\d+(\.\d+)?", - "func": float, - }, - "uuid": { - "regex": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - "func": uuid.UUID, - }, - "path": { - "regex": r".+", - "func": str, - }, -} -"""The supported conversion types""" - - -router = create_router(SimpleResolver) -"""The simple router""" diff --git a/src/reactpy_router/static/link.js b/src/reactpy_router/static/link.js new file mode 100644 index 0000000..0ce08b9 --- /dev/null +++ b/src/reactpy_router/static/link.js @@ -0,0 +1,8 @@ +document.querySelector(".UUID").addEventListener( + "click", + (event) => { + let to = event.target.getAttribute("href"); + window.history.pushState({}, to, new URL(to, window.location)); + }, + { once: true }, +); diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index a91787e..15a77c4 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -1,27 +1,30 @@ -"""Types for reactpy_router""" +"""Type definitions for the `reactpy-router` package.""" from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Sequence, TypeVar +from typing import Any, Callable, Sequence, TypedDict, TypeVar from reactpy.core.vdom import is_vdom from reactpy.types import ComponentType, Key -from typing_extensions import Protocol, Self +from typing_extensions import Protocol, Self, TypeAlias + +ConversionFunc: TypeAlias = Callable[[str], Any] +ConverterMapping: TypeAlias = dict[str, ConversionFunc] @dataclass(frozen=True) class Route: - """A route that can be matched against a path""" + """A route that can be matched against a path.""" path: str - """The path to match against""" + """The path to match against.""" element: Any = field(hash=False) - """The element to render if the path matches""" + """The element to render if the path matches.""" routes: Sequence[Self] - """Child routes""" + """Child routes.""" def __hash__(self) -> int: el = self.element @@ -29,29 +32,37 @@ def __hash__(self) -> int: return hash((self.path, key, self.routes)) -R = TypeVar("R", bound=Route, contravariant=True) +RouteType = TypeVar("RouteType", bound=Route) +RouteType_contra = TypeVar("RouteType_contra", bound=Route, contravariant=True) -class Router(Protocol[R]): - """Return a component that renders the first matching route""" +class Router(Protocol[RouteType_contra]): + """Return a component that renders the first matching route.""" - def __call__(self, *routes: R) -> ComponentType: - ... + def __call__(self, *routes: RouteType_contra) -> ComponentType: ... -class RouteCompiler(Protocol[R]): - """Compile a route into a resolver that can be matched against a path""" +class Resolver(Protocol[RouteType_contra]): + """Compile a route into a resolver that can be matched against a given path.""" - def __call__(self, route: R) -> RouteResolver: - ... + def __call__(self, route: RouteType_contra) -> CompiledRoute: ... -class RouteResolver(Protocol): - """A compiled route that can be matched against a path""" +class CompiledRoute(Protocol): + """A compiled route that can be matched against a path.""" @property def key(self) -> Key: - """Uniquely identified this resolver""" + """Uniquely identified this resolver.""" def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: - """Return the path's associated element and path params or None""" + """Return the path's associated element and path parameters or None.""" + + +class ConversionInfo(TypedDict): + """Information about a conversion type.""" + + regex: str + """The regex to match the conversion type.""" + func: ConversionFunc + """The function to convert the matched string to the expected type.""" diff --git a/tests/conftest.py b/tests/conftest.py index 573eba5..18e3646 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,31 +1,44 @@ +import asyncio +import os +import sys + import pytest from playwright.async_api import async_playwright from reactpy.testing import BackendFixture, DisplayFixture +GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "").lower() == "true" + def pytest_addoption(parser) -> None: parser.addoption( - "--headed", - dest="headed", + "--headless", + dest="headless", action="store_true", - help="Open a browser window when runnging web-based tests", + help="Hide the browser window when running web-based tests", ) @pytest.fixture async def display(backend, browser): - async with DisplayFixture(backend, browser) as display: - display.page.set_default_timeout(10000) - yield display + async with DisplayFixture(backend, browser) as display_fixture: + display_fixture.page.set_default_timeout(10000) + yield display_fixture @pytest.fixture async def backend(): - async with BackendFixture() as backend: - yield backend + async with BackendFixture() as backend_fixture: + yield backend_fixture @pytest.fixture async def browser(pytestconfig): async with async_playwright() as pw: - yield await pw.chromium.launch(headless=not bool(pytestconfig.option.headed)) + yield await pw.chromium.launch(headless=True if GITHUB_ACTIONS else pytestconfig.getoption("headless")) + + +@pytest.fixture +def event_loop_policy(request): + if sys.platform == "win32": + return asyncio.WindowsProactorEventLoopPolicy() + return asyncio.get_event_loop_policy() diff --git a/tests/test_core.py b/tests/test_core.py index 77577b3..390236d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,8 +1,13 @@ +import os from typing import Any from reactpy import Ref, component, html, use_location from reactpy.testing import DisplayFixture -from reactpy_router import link, route, simple, use_params, use_query + +from reactpy_router import browser_router, link, route, use_params, use_search_params + +GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "").lower() == "true" +CLICK_DELAY = 350 if GITHUB_ACTIONS else 25 # Delay in miliseconds. async def test_simple_router(display: DisplayFixture): @@ -18,7 +23,7 @@ def check_location(): @component def sample(): - return simple.router( + return browser_router( make_location_check("/a"), make_location_check("/b"), make_location_check("/c"), @@ -40,7 +45,8 @@ def sample(): root_element = await display.root_element() except AttributeError: root_element = await display.page.wait_for_selector( - f"#display-{display._next_view_id}", state="attached" # type: ignore + f"#display-{display._next_view_id}", # type: ignore + state="attached", ) assert not await root_element.inner_html() @@ -49,7 +55,7 @@ def sample(): async def test_nested_routes(display: DisplayFixture): @component def sample(): - return simple.router( + return browser_router( route( "/a", html.h1({"id": "a"}, "A"), @@ -78,19 +84,19 @@ async def test_navigate_with_link(display: DisplayFixture): @component def sample(): render_count.current += 1 - return simple.router( - route("/", link("Root", to="/a", id="root")), - route("/a", link("A", to="/b", id="a")), - route("/b", link("B", to="/c", id="b")), - route("/c", link("C", to="/default", id="c")), - route("*", html.h1({"id": "default"}, "Default")), + return browser_router( + route("/", link({"to": "/a", "id": "root"}, "Root")), + route("/a", link({"to": "/b", "id": "a"}, "A")), + route("/b", link({"to": "/c", "id": "b"}, "B")), + route("/c", link({"to": "/default", "id": "c"}, "C")), + route("{default:any}", html.h1({"id": "default"}, "Default")), ) await display.show(sample) for link_selector in ["#root", "#a", "#b", "#c"]: - lnk = await display.page.wait_for_selector(link_selector) - await lnk.click() + _link = await display.page.wait_for_selector(link_selector) + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") @@ -109,7 +115,7 @@ def check_params(): @component def sample(): - return simple.router( + return browser_router( route( "/first/{first:str}", check_params(), @@ -135,17 +141,17 @@ def sample(): await display.page.wait_for_selector("#success") -async def test_use_query(display: DisplayFixture): +async def test_search_params(display: DisplayFixture): expected_query: dict[str, Any] = {} @component def check_query(): - assert use_query() == expected_query + assert use_search_params() == expected_query return html.h1({"id": "success"}, "success") @component def sample(): - return simple.router(route("/", check_query())) + return browser_router(route("/", check_query())) await display.show(sample) @@ -157,19 +163,19 @@ def sample(): async def test_browser_popstate(display: DisplayFixture): @component def sample(): - return simple.router( - route("/", link("Root", to="/a", id="root")), - route("/a", link("A", to="/b", id="a")), - route("/b", link("B", to="/c", id="b")), - route("/c", link("C", to="/default", id="c")), - route("*", html.h1({"id": "default"}, "Default")), + return browser_router( + route("/", link({"to": "/a", "id": "root"}, "Root")), + route("/a", link({"to": "/b", "id": "a"}, "A")), + route("/b", link({"to": "/c", "id": "b"}, "B")), + route("/c", link({"to": "/default", "id": "c"}, "C")), + route("{default:any}", html.h1({"id": "default"}, "Default")), ) await display.show(sample) for link_selector in ["#root", "#a", "#b", "#c"]: - lnk = await display.page.wait_for_selector(link_selector) - await lnk.click() + _link = await display.page.wait_for_selector(link_selector) + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") @@ -189,24 +195,28 @@ def sample(): async def test_relative_links(display: DisplayFixture): @component def sample(): - return simple.router( - route("/", link("Root", to="/a", id="root")), - route("/a", link("A", to="/a/b", id="a")), - route("/a/b", link("B", to="../a/b/c", id="b")), - route("/a/b/c", link("C", to="../d", id="c")), - route("/a/d", link("D", to="e", id="d")), - route("/a/e", link("E", to="../default", id="e")), - route("*", html.h1({"id": "default"}, "Default")), + return browser_router( + route("/", link({"to": "a", "id": "root"}, "Root")), + route("/a", link({"to": "/a/a/../b", "id": "a"}, "A")), + route("/a/b", link({"to": "../a/b/c", "id": "b"}, "B")), + route("/a/b/c", link({"to": "../d", "id": "c"}, "C")), + route("/a/d", link({"to": "e", "id": "d"}, "D")), + route("/a/e", link({"to": "/a/./f", "id": "e"}, "E")), + route("/a/f", link({"to": "../default", "id": "f"}, "F")), + route("{default:any}", html.h1({"id": "default"}, "Default")), ) await display.show(sample) - for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e"]: - lnk = await display.page.wait_for_selector(link_selector) - await lnk.click() + for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e", "#f"]: + _link = await display.page.wait_for_selector(link_selector) + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") + await display.page.go_back() + await display.page.wait_for_selector("#f") + await display.page.go_back() await display.page.wait_for_selector("#e") @@ -224,3 +234,46 @@ def sample(): await display.page.go_back() await display.page.wait_for_selector("#root") + + +async def test_link_with_query_string(display: DisplayFixture): + @component + def check_search_params(): + query = use_search_params() + assert query == {"a": ["1"], "b": ["2"]} + return html.h1({"id": "success"}, "success") + + @component + def sample(): + return browser_router( + route("/", link({"to": "/a?a=1&b=2", "id": "root"}, "Root")), + route("/a", check_search_params()), + ) + + await display.show(sample) + await display.page.wait_for_selector("#root") + _link = await display.page.wait_for_selector("#root") + await _link.click(delay=CLICK_DELAY) + await display.page.wait_for_selector("#success") + + +async def test_link_class_name(display: DisplayFixture): + @component + def sample(): + return browser_router(route("/", link({"to": "/a", "id": "root", "className": "class1"}, "Root"))) + + await display.show(sample) + + _link = await display.page.wait_for_selector("#root") + assert "class1" in await _link.get_attribute("class") + + +async def test_link_href(display: DisplayFixture): + @component + def sample(): + return browser_router(route("/", link({"href": "/a", "id": "root"}, "Root"))) + + await display.show(sample) + + _link = await display.page.wait_for_selector("#root") + assert "/a" in await _link.get_attribute("href") diff --git a/tests/test_resolver.py b/tests/test_resolver.py new file mode 100644 index 0000000..4e8a669 --- /dev/null +++ b/tests/test_resolver.py @@ -0,0 +1,57 @@ +import re +import uuid + +import pytest + +from reactpy_router import route +from reactpy_router.resolvers import StarletteResolver + + +def test_resolve_any(): + resolver = StarletteResolver(route("{404:any}", "Hello World")) + assert resolver.parse_path("{404:any}") == re.compile("^(?P<_numeric_404>.*)$") + assert resolver.converter_mapping == {"_numeric_404": str} + assert resolver.resolve("/hello/world") == ("Hello World", {"404": "/hello/world"}) + + +def test_parse_path(): + resolver = StarletteResolver(route("/", None)) + assert resolver.parse_path("/a/b/c") == re.compile("^/a/b/c$") + assert resolver.converter_mapping == {} + + assert resolver.parse_path("/a/{b}/c") == re.compile(r"^/a/(?P[^/]+)/c$") + assert resolver.converter_mapping == {"b": str} + + assert resolver.parse_path("/a/{b:int}/c") == re.compile(r"^/a/(?P\d+)/c$") + assert resolver.converter_mapping == {"b": int} + + assert resolver.parse_path("/a/{b:int}/{c:float}/c") == re.compile(r"^/a/(?P\d+)/(?P\d+(\.\d+)?)/c$") + assert resolver.converter_mapping == {"b": int, "c": float} + + assert resolver.parse_path("/a/{b:int}/{c:float}/{d:uuid}/c") == re.compile( + r"^/a/(?P\d+)/(?P\d+(\.\d+)?)/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/c$" + ) + assert resolver.converter_mapping == {"b": int, "c": float, "d": uuid.UUID} + + assert resolver.parse_path("/a/{b:int}/{c:float}/{d:uuid}/{e:path}/c") == re.compile( + r"^/a/(?P\d+)/(?P\d+(\.\d+)?)/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/(?P.+)/c$" + ) + assert resolver.converter_mapping == { + "b": int, + "c": float, + "d": uuid.UUID, + "e": str, + } + + +def test_parse_path_unkown_conversion(): + resolver = StarletteResolver(route("/", None)) + with pytest.raises(ValueError): + resolver.parse_path("/a/{b:unknown}/c") + + +def test_parse_path_re_escape(): + """Check that we escape regex characters in the path""" + resolver = StarletteResolver(route("/", None)) + assert resolver.parse_path("/a/{b:int}/c.d") == re.compile(r"^/a/(?P\d+)/c\.d$") + assert resolver.converter_mapping == {"b": int} diff --git a/tests/test_simple.py b/tests/test_simple.py deleted file mode 100644 index 9ec8a2b..0000000 --- a/tests/test_simple.py +++ /dev/null @@ -1,54 +0,0 @@ -import re -import uuid - -import pytest - -from reactpy_router.simple import parse_path - - -def test_parse_path(): - assert parse_path("/a/b/c") == (re.compile("^/a/b/c$"), {}) - assert parse_path("/a/{b}/c") == ( - re.compile(r"^/a/(?P[^/]+)/c$"), - {"b": str}, - ) - assert parse_path("/a/{b:int}/c") == ( - re.compile(r"^/a/(?P\d+)/c$"), - {"b": int}, - ) - assert parse_path("/a/{b:int}/{c:float}/c") == ( - re.compile(r"^/a/(?P\d+)/(?P\d+(\.\d+)?)/c$"), - {"b": int, "c": float}, - ) - assert parse_path("/a/{b:int}/{c:float}/{d:uuid}/c") == ( - re.compile( - r"^/a/(?P\d+)/(?P\d+(\.\d+)?)/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[" - r"0-9a-f]{4}-[0-9a-f]{12})/c$" - ), - {"b": int, "c": float, "d": uuid.UUID}, - ) - assert parse_path("/a/{b:int}/{c:float}/{d:uuid}/{e:path}/c") == ( - re.compile( - r"^/a/(?P\d+)/(?P\d+(\.\d+)?)/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[" - r"0-9a-f]{4}-[0-9a-f]{12})/(?P.+)/c$" - ), - {"b": int, "c": float, "d": uuid.UUID, "e": str}, - ) - - -def test_parse_path_unkown_conversion(): - with pytest.raises(ValueError): - parse_path("/a/{b:unknown}/c") - - -def test_parse_path_re_escape(): - """Check that we escape regex characters in the path""" - assert parse_path("/a/{b:int}/c.d") == ( - # ^ regex character - re.compile(r"^/a/(?P\d+)/c\.d$"), - {"b": int}, - ) - - -def test_match_star_path(): - assert parse_path("*") == (re.compile("^.*$"), {}) From 0c04073019e1e49d850dc51869dc72ff646265d2 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 14 Oct 2024 02:36:57 -0700 Subject: [PATCH 07/22] Fix ctrl click on links (#33) --- CHANGELOG.md | 3 +-- src/reactpy_router/components.py | 6 ++++-- src/reactpy_router/static/link.js | 8 ++++++-- tests/{test_core.py => test_router.py} | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) rename tests/{test_core.py => test_router.py} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7380865..f652ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,6 @@ Using the following categories, list your changes in this order: ### Changed -- Bump GitHub workflows - Rename `use_query` to `use_search_params`. - Rename `simple.router` to `browser_router`. - Rename `SimpleResolver` to `StarletteResolver`. @@ -58,7 +57,7 @@ Using the following categories, list your changes in this order: - Fix bug where "Match Any" pattern wouldn't work when used in complex or nested paths. - Fix bug where `link` elements could not have `@component` type children. - Fix bug where the ReactPy would not detect the current URL after a reconnection. -- Fixed flakey tests on GitHub CI by adding click delays. +- Fix bug where `ctrl` + `click` on a `link` element would not open in a new tab. ## [0.1.1] - 2023-12-13 diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py index 3008065..6c023d4 100644 --- a/src/reactpy_router/components.py +++ b/src/reactpy_router/components.py @@ -5,7 +5,7 @@ from urllib.parse import urljoin from uuid import uuid4 -from reactpy import component, event, html, use_connection +from reactpy import component, html, use_connection from reactpy.backend.types import Location from reactpy.core.component import Component from reactpy.core.types import VdomDict @@ -63,8 +63,10 @@ def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: # https://github.com/reactive-python/reactpy/pull/1224 current_path = use_connection().location.pathname - @event(prevent_default=True) def on_click(_event: dict[str, Any]) -> None: + if _event.get("ctrlKey", False): + return + pathname, search = to.split("?", 1) if "?" in to else (to, "") if search: search = f"?{search}" diff --git a/src/reactpy_router/static/link.js b/src/reactpy_router/static/link.js index 0ce08b9..b574201 100644 --- a/src/reactpy_router/static/link.js +++ b/src/reactpy_router/static/link.js @@ -1,8 +1,12 @@ document.querySelector(".UUID").addEventListener( "click", (event) => { - let to = event.target.getAttribute("href"); - window.history.pushState({}, to, new URL(to, window.location)); + // Prevent default if ctrl isn't pressed + if (!event.ctrlKey) { + event.preventDefault(); + let to = event.target.getAttribute("href"); + window.history.pushState({}, to, new URL(to, window.location)); + } }, { once: true }, ); diff --git a/tests/test_core.py b/tests/test_router.py similarity index 93% rename from tests/test_core.py rename to tests/test_router.py index 390236d..d6e0deb 100644 --- a/tests/test_core.py +++ b/tests/test_router.py @@ -1,6 +1,7 @@ import os from typing import Any +from playwright.async_api._generated import Browser, Page from reactpy import Ref, component, html, use_location from reactpy.testing import DisplayFixture @@ -277,3 +278,20 @@ def sample(): _link = await display.page.wait_for_selector("#root") assert "/a" in await _link.get_attribute("href") + + +async def test_ctrl_click(display: DisplayFixture, browser: Browser): + @component + def sample(): + return browser_router( + route("/", link({"to": "/a", "id": "root"}, "Root")), + route("/a", link({"to": "/a", "id": "a"}, "a")), + ) + + await display.show(sample) + + _link = await display.page.wait_for_selector("#root") + await _link.click(delay=CLICK_DELAY, modifiers=["Control"]) + browser_context = browser.contexts[0] + new_page: Page = await browser_context.wait_for_event("page") + await new_page.wait_for_selector("#a") From 2f9be017c40e4b8a84c41b49ae221c2d8fc2b15e Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 17 Oct 2024 00:36:16 -0700 Subject: [PATCH 08/22] New `Navigate` component (#34) - Create `navigate` component - Better key value identity of route components - Remove initial URL handling from the `History` component due to reactpy rendering bugs - This is now handled by a new `FirstLoad` element. - Fix docs publishing workflow - Add arg descriptions to all public functions - Better styling for autodocs. - Support Python 3.12 --- .github/workflows/test-docs.yml | 1 - .github/workflows/test-src.yaml | 70 ++++----- CHANGELOG.md | 8 +- .../python/basic-routing-more-routes.py | 1 + docs/examples/python/basic-routing.py | 1 + docs/mkdocs.yml | 17 +- docs/src/dictionary.txt | 1 + docs/src/reference/components.md | 2 +- docs/src/reference/{router.md => routers.md} | 0 docs/src/reference/types.md | 4 + pyproject.toml | 1 - requirements/build-docs.txt | 2 + requirements/test-env.txt | 2 +- src/js/src/index.js | 106 ++++++++++--- src/reactpy_router/__init__.py | 3 +- src/reactpy_router/components.py | 71 ++++++++- src/reactpy_router/hooks.py | 33 ++-- src/reactpy_router/routers.py | 60 +++++-- src/reactpy_router/static/link.js | 7 +- src/reactpy_router/types.py | 95 +++++++++-- tests/conftest.py | 16 +- tests/test_router.py | 147 +++++++++++++----- 22 files changed, 486 insertions(+), 162 deletions(-) rename docs/src/reference/{router.md => routers.md} (100%) diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 7110bc4..df91de1 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -25,7 +25,6 @@ jobs: pip install -r requirements/build-docs.txt pip install -r requirements/check-types.txt pip install -r requirements/check-style.txt - pip install -e . - name: Check docs build run: | linkcheckMarkdown docs/ -v -r diff --git a/.github/workflows/test-src.yaml b/.github/workflows/test-src.yaml index df93152..687741b 100644 --- a/.github/workflows/test-src.yaml +++ b/.github/workflows/test-src.yaml @@ -1,40 +1,40 @@ name: Test on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "0 0 * * *" + 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"] - 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 - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Latest Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install Python Dependencies - run: pip install -r requirements/test-run.txt - - name: Run Tests - run: nox -t test -- --coverage + 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 + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Latest Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install Python Dependencies + run: pip install -r requirements/test-run.txt + - name: Run Tests + run: nox -t test -- --coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index f652ffa..bf4e08c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,14 +42,15 @@ Using the following categories, list your changes in this order: - Rename `CONVERSION_TYPES` to `CONVERTERS`. - Change "Match Any" syntax from a star `*` to `{name:any}`. - Rewrite `reactpy_router.link` to be a server-side component. -- Simplified top-level exports within `reactpy_router`. +- Simplified top-level exports that are available within `reactpy_router.*`. ### Added -- New error for ReactPy router elements being used outside router context. -- Configurable/inheritable `Resolver` base class. - Add debug log message for when there are no router matches. - Add slug as a supported type. +- Add `reactpy_router.navigate` component that will force the client to navigate to a new URL (when rendered). +- New error for ReactPy router elements being used outside router context. +- Configurable/inheritable `Resolver` base class. ### Fixed @@ -58,6 +59,7 @@ Using the following categories, list your changes in this order: - Fix bug where `link` elements could not have `@component` type children. - Fix bug where the ReactPy would not detect the current URL after a reconnection. - Fix bug where `ctrl` + `click` on a `link` element would not open in a new tab. +- Fix test suite on Windows machines. ## [0.1.1] - 2023-12-13 diff --git a/docs/examples/python/basic-routing-more-routes.py b/docs/examples/python/basic-routing-more-routes.py index 32bb31e..14c9b5a 100644 --- a/docs/examples/python/basic-routing-more-routes.py +++ b/docs/examples/python/basic-routing-more-routes.py @@ -1,4 +1,5 @@ from reactpy import component, html, run + from reactpy_router import browser_router, route diff --git a/docs/examples/python/basic-routing.py b/docs/examples/python/basic-routing.py index 43c4e65..efc7835 100644 --- a/docs/examples/python/basic-routing.py +++ b/docs/examples/python/basic-routing.py @@ -1,4 +1,5 @@ from reactpy import component, html, run + from reactpy_router import browser_router, route diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 5173834..ebf8b0e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -8,7 +8,7 @@ nav: - Hooks: learn/hooks.md - Creating a Custom Router 🚧: learn/custom-router.md - Reference: - - Router Components: reference/router.md + - Routers: reference/routers.md - Components: reference/components.md - Hooks: reference/hooks.md - Types: reference/types.md @@ -96,8 +96,21 @@ plugins: - https://reactpy.dev/docs/objects.inv - https://installer.readthedocs.io/en/stable/objects.inv options: - show_bases: false + signature_crossrefs: true + scoped_crossrefs: true + relative_crossrefs: true + modernize_annotations: true + unwrap_annotated: true + find_stubs_package: true show_root_members_full_path: true + show_bases: false + show_source: false + show_root_toc_entry: false + show_labels: false + show_symbol_type_toc: true + show_symbol_type_heading: true + show_object_full_path: true + heading_level: 3 extra: generator: false version: diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 6eb9552..64ed74d 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -37,3 +37,4 @@ misconfiguration misconfigurations backhaul sublicense +contravariant diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index f1cc570..9841110 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -1,4 +1,4 @@ ::: reactpy_router options: - members: ["route", "link"] + members: ["route", "link", "navigate"] diff --git a/docs/src/reference/router.md b/docs/src/reference/routers.md similarity index 100% rename from docs/src/reference/router.md rename to docs/src/reference/routers.md diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index 204bee7..3898ae8 100644 --- a/docs/src/reference/types.md +++ b/docs/src/reference/types.md @@ -1 +1,5 @@ ::: reactpy_router.types + + options: + summary: true + docstring_section_style: "list" diff --git a/pyproject.toml b/pyproject.toml index d6a0110..09826fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,4 +17,3 @@ line-length = 120 [tool.pytest.ini_options] testpaths = "tests" -asyncio_mode = "auto" diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt index 0d2bca2..57805cb 100644 --- a/requirements/build-docs.txt +++ b/requirements/build-docs.txt @@ -9,3 +9,5 @@ mkdocs-minify-plugin mkdocs-section-index mike mkdocstrings[python] +black # for mkdocstrings automatic code formatting +. diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 4ddd635..4b78ca5 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1,6 +1,6 @@ twine pytest -pytest-asyncio +anyio pytest-cov reactpy[testing,starlette] nodejs-bin==18.4.0a4 diff --git a/src/js/src/index.js b/src/js/src/index.js index 8ead7eb..4e7f02f 100644 --- a/src/js/src/index.js +++ b/src/js/src/index.js @@ -12,13 +12,23 @@ export function bind(node) { }; } -export function History({ onHistoryChange }) { - // Capture browser "history go back" action and tell the server about it - // Note: Browsers do not allow us to detect "history go forward" actions. +/** + * History component that captures browser "history go back" actions and notifies the server. + * + * @param {Object} props - The properties object. + * @param {Function} props.onHistoryChangeCallback - Callback function to notify the server about history changes. + * @returns {null} This component does not render any visible output. + * @description + * This component uses the `popstate` event to detect when the user navigates back in the browser history. + * It then calls the `onHistoryChangeCallback` with the current pathname and search parameters. + * Note: Browsers do not allow detection of "history go forward" actions. + * @see https://github.com/reactive-python/reactpy/pull/1224 + */ +export function History({ onHistoryChangeCallback }) { React.useEffect(() => { // Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback. const listener = () => { - onHistoryChange({ + onHistoryChangeCallback({ pathname: window.location.pathname, search: window.location.search, }); @@ -32,22 +42,33 @@ export function History({ onHistoryChange }) { }); // Tell the server about the URL during the initial page load - // FIXME: This currently runs every time any component is mounted due to a ReactPy core rendering bug. + // FIXME: This code is commented out since it currently runs every time any component + // is mounted due to a ReactPy core rendering bug. `FirstLoad` component is used instead. // https://github.com/reactive-python/reactpy/pull/1224 - React.useEffect(() => { - onHistoryChange({ - pathname: window.location.pathname, - search: window.location.search, - }); - return () => {}; - }, []); + + // React.useEffect(() => { + // onHistoryChange({ + // pathname: window.location.pathname, + // search: window.location.search, + // }); + // return () => {}; + // }, []); return null; } -// FIXME: The Link component is unused due to a ReactPy core rendering bug -// which causes duplicate rendering (and thus duplicate event listeners). -// https://github.com/reactive-python/reactpy/pull/1224 -export function Link({ onClick, linkClass }) { +/** + * Link component that captures clicks on anchor links and notifies the server. + * + * @param {Object} props - The properties object. + * @param {Function} props.onClickCallback - Callback function to notify the server about link clicks. + * @param {string} props.linkClass - The class name of the anchor link. + * @returns {null} This component does not render any visible output. + */ +export function Link({ onClickCallback, linkClass }) { + // FIXME: This component is currently unused due to a ReactPy core rendering bug + // which causes duplicate rendering (and thus duplicate event listeners). + // https://github.com/reactive-python/reactpy/pull/1224 + // This component is not the actual anchor link. // It is an event listener for the link component created by ReactPy. React.useEffect(() => { @@ -55,8 +76,8 @@ export function Link({ onClick, linkClass }) { const handleClick = (event) => { event.preventDefault(); let to = event.target.getAttribute("href"); - window.history.pushState({}, to, new URL(to, window.location)); - onClick({ + window.history.pushState(null, "", new URL(to, window.location)); + onClickCallback({ pathname: window.location.pathname, search: window.location.search, }); @@ -78,3 +99,52 @@ export function Link({ onClick, linkClass }) { }); return null; } + +/** + * Client-side portion of the navigate component, that allows the server to command the client to change URLs. + * + * @param {Object} props - The properties object. + * @param {Function} props.onNavigateCallback - Callback function that transmits data to the server. + * @param {string} props.to - The target URL to navigate to. + * @param {boolean} props.replace - If true, replaces the current history entry instead of adding a new one. + * @returns {null} This component does not render anything. + */ +export function Navigate({ onNavigateCallback, to, replace }) { + React.useEffect(() => { + if (replace) { + window.history.replaceState(null, "", new URL(to, window.location)); + } else { + window.history.pushState(null, "", new URL(to, window.location)); + } + onNavigateCallback({ + pathname: window.location.pathname, + search: window.location.search, + }); + return () => {}; + }, []); + + return null; +} + +/** + * FirstLoad component that captures the URL during the initial page load and notifies the server. + * + * @param {Object} props - The properties object. + * @param {Function} props.onFirstLoadCallback - Callback function to notify the server about the first load. + * @returns {null} This component does not render any visible output. + * @description + * This component sends the current URL to the server during the initial page load. + * @see https://github.com/reactive-python/reactpy/pull/1224 + */ +export function FirstLoad({ onFirstLoadCallback }) { + // FIXME: This component only exists because of a ReactPy core rendering bug, and should be removed when the bug + // is fixed. Ideally all this logic would be handled by the `History` component. + React.useEffect(() => { + onFirstLoadCallback({ + pathname: window.location.pathname, + search: window.location.search, + }); + return () => {}; + }, []); + return null; +} diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index fa2781f..9f272c2 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -2,7 +2,7 @@ __version__ = "0.1.1" -from .components import link, route +from .components import link, navigate, route from .hooks import use_params, use_search_params from .routers import browser_router, create_router @@ -13,4 +13,5 @@ "browser_router", "use_params", "use_search_params", + "navigate", ) diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py index 6c023d4..657c558 100644 --- a/src/reactpy_router/components.py +++ b/src/reactpy_router/components.py @@ -26,17 +26,36 @@ ) """Client-side portion of link handling""" +Navigate = export( + module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), + ("Navigate"), +) +"""Client-side portion of the navigate component""" + +FirstLoad = export( + module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), + ("FirstLoad"), +) + link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8") def link(attributes: dict[str, Any], *children: Any) -> Component: - """Create a link with the given attributes and children.""" + """ + Create a link with the given attributes and children. + + Args: + attributes: A dictionary of attributes for the link. + *children: Child elements to be included within the link. + + Returns: + A link component with the specified attributes and children. + """ return _link(attributes, *children) @component def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: - """A component that renders a link to the given path.""" attributes = attributes.copy() uuid_string = f"link-{uuid4().hex}" class_name = f"{uuid_string}" @@ -93,11 +112,53 @@ def on_click(_event: dict[str, Any]) -> None: return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string))) - # def on_click(_event: dict[str, Any]) -> None: + # def on_click_callback(_event: dict[str, Any]) -> None: # set_location(Location(**_event)) - # return html._(html.a(attrs, *children), Link({"onClick": on_click, "linkClass": uuid_string})) + # return html._(html.a(attrs, *children), Link({"onClickCallback": on_click_callback, "linkClass": uuid_string})) def route(path: str, element: Any | None, *routes: Route) -> Route: - """Create a route with the given path, element, and child routes.""" + """ + Create a route with the given path, element, and child routes. + + Args: + path: The path for the route. + element: The element to render for this route. Can be None. + routes: Additional child routes. + + Returns: + The created route object. + """ return Route(path, element, routes) + + +def navigate(to: str, replace: bool = False) -> Component: + """ + Navigate to a specified URL. + + This function changes the browser's current URL when it is rendered. + + Args: + to: The target URL to navigate to. + replace: If True, the current history entry will be replaced \ + with the new URL. Defaults to False. + + Returns: + The component responsible for navigation. + """ + return _navigate(to, replace) + + +@component +def _navigate(to: str, replace: bool = False) -> VdomDict | None: + location = use_connection().location + set_location = _use_route_state().set_location + pathname = to.split("?", 1)[0] + + def on_navigate_callback(_event: dict[str, Any]) -> None: + set_location(Location(**_event)) + + if location.pathname != pathname: + return Navigate({"onNavigateCallback": on_navigate_callback, "to": to, "replace": replace}) + + return None diff --git a/src/reactpy_router/hooks.py b/src/reactpy_router/hooks.py index 3831acf..add8953 100644 --- a/src/reactpy_router/hooks.py +++ b/src/reactpy_router/hooks.py @@ -1,21 +1,17 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Any, Callable +from typing import Any from urllib.parse import parse_qs from reactpy import create_context, use_context, use_location -from reactpy.backend.types import Location from reactpy.types import Context +from reactpy_router.types import RouteState -@dataclass -class _RouteState: - set_location: Callable[[Location], None] - params: dict[str, Any] +_route_state_context: Context[RouteState | None] = create_context(None) -def _use_route_state() -> _RouteState: +def _use_route_state() -> RouteState: route_state = use_context(_route_state_context) if route_state is None: # pragma: no cover raise RuntimeError( @@ -26,16 +22,17 @@ def _use_route_state() -> _RouteState: return route_state -_route_state_context: Context[_RouteState | None] = create_context(None) - - def use_params() -> dict[str, Any]: - """The `use_params` hook returns an object of key/value pairs of the dynamic parameters \ + """This hook returns an object of key/value pairs of the dynamic parameters \ from the current URL that were matched by the `Route`. Child routes inherit all parameters \ from their parent routes. For example, if you have a `URL_PARAM` defined in the route `/example//`, - this hook will return the URL_PARAM value that was matched.""" + this hook will return the `URL_PARAM` value that was matched. + + Returns: + A dictionary of the current URL's parameters. + """ # TODO: Check if this returns all parent params return _use_route_state().params @@ -49,10 +46,14 @@ def use_search_params( separator: str = "&", ) -> dict[str, list[str]]: """ - The `use_search_params` hook is used to read the query string in the URL \ - for the current location. + This hook is used to read the query string in the URL for the current location. + + See [`urllib.parse.parse_qs`](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.parse_qs) \ + for info on this hook's parameters. - See `urllib.parse.parse_qs` for info on this hook's parameters.""" + Returns: + A dictionary of the current URL's query string parameters. + """ location = use_location() query_string = location.search[1:] if len(location.search) > 1 else "" diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index 25b72c1..a815f0d 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -4,16 +4,16 @@ from dataclasses import replace from logging import getLogger -from typing import Any, Iterator, Literal, Sequence +from typing import Any, Iterator, Literal, Sequence, cast from reactpy import component, use_memo, use_state from reactpy.backend.hooks import ConnectionContext, use_connection from reactpy.backend.types import Connection, Location -from reactpy.core.types import VdomDict -from reactpy.types import ComponentType +from reactpy.core.component import Component +from reactpy.types import ComponentType, VdomDict -from reactpy_router.components import History -from reactpy_router.hooks import _route_state_context, _RouteState +from reactpy_router.components import FirstLoad, History +from reactpy_router.hooks import RouteState, _route_state_context from reactpy_router.resolvers import StarletteResolver from reactpy_router.types import CompiledRoute, Resolver, Router, RouteType @@ -24,15 +24,27 @@ def create_router(resolver: Resolver[RouteType]) -> Router[RouteType]: """A decorator that turns a resolver into a router""" - def wrapper(*routes: RouteType) -> ComponentType: + def wrapper(*routes: RouteType) -> Component: return router(*routes, resolver=resolver) return wrapper -browser_router = create_router(StarletteResolver) -"""This is the recommended router for all ReactPy Router web projects. -It uses the JavaScript DOM History API to manage the history stack.""" +_starlette_router = create_router(StarletteResolver) + + +def browser_router(*routes: RouteType) -> Component: + """This is the recommended router for all ReactPy-Router web projects. + It uses the JavaScript [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) + to manage the history stack. + + Args: + *routes (RouteType): A list of routes to be rendered by the router. + + Returns: + A router component that renders the given routes. + """ + return _starlette_router(*routes) @component @@ -47,6 +59,7 @@ def router( old_conn = use_connection() location, set_location = use_state(old_conn.location) + first_load, set_first_load = use_state(True) resolvers = use_memo( lambda: tuple(map(resolver, _iter_routes(routes))), @@ -59,7 +72,7 @@ def router( route_elements = [ _route_state_context( element, - value=_RouteState(set_location, params), + value=RouteState(set_location, params), ) for element, params in match ] @@ -70,8 +83,15 @@ def on_history_change(event: dict[str, Any]) -> None: if location != new_location: set_location(new_location) + def on_first_load(event: dict[str, Any]) -> None: + """Callback function used within the JavaScript `FirstLoad` component.""" + if first_load: + set_first_load(False) + on_history_change(event) + return ConnectionContext( - History({"onHistoryChange": on_history_change}), # type: ignore[return-value] + History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value] + FirstLoad({"onFirstLoadCallback": on_first_load}) if first_load else "", *route_elements, value=Connection(old_conn.scope, location, old_conn.carrier), ) @@ -86,6 +106,18 @@ def _iter_routes(routes: Sequence[RouteType]) -> Iterator[RouteType]: yield parent +def _add_route_key(match: tuple[Any, dict[str, Any]], key: str | int) -> Any: + """Add a key to the VDOM or component on the current route, if it doesn't already have one.""" + element, _params = match + if hasattr(element, "render") and not element.key: + element = cast(ComponentType, element) + element.key = key + elif isinstance(element, dict) and not element.get("key", None): + element = cast(VdomDict, element) + element["key"] = key + return match + + def _match_route( compiled_routes: Sequence[CompiledRoute], location: Location, @@ -97,12 +129,14 @@ def _match_route( match = resolver.resolve(location.pathname) if match is not None: if select == "first": - return [match] + return [_add_route_key(match, resolver.key)] # Matching multiple routes is disabled since `react-router` no longer supports multiple # matches via the `Route` component. However, it's kept here to support future changes # or third-party routers. - matches.append(match) # pragma: no cover + # TODO: The `resolver.key` value has edge cases where it is not unique enough to use as + # a key here. We can potentially fix this by throwing errors for duplicate identical routes. + matches.append(_add_route_key(match, resolver.key)) # pragma: no cover if not matches: _logger.debug("No matching route found for %s", location.pathname) diff --git a/src/reactpy_router/static/link.js b/src/reactpy_router/static/link.js index b574201..9f78cc5 100644 --- a/src/reactpy_router/static/link.js +++ b/src/reactpy_router/static/link.js @@ -5,7 +5,12 @@ document.querySelector(".UUID").addEventListener( if (!event.ctrlKey) { event.preventDefault(); let to = event.target.getAttribute("href"); - window.history.pushState({}, to, new URL(to, window.location)); + let new_url = new URL(to, window.location); + + // Deduplication needed due to ReactPy rendering bug + if (new_url.href !== window.location.href) { + window.history.pushState(null, "", new URL(to, window.location)); + } } }, { once: true }, diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index 15a77c4..87f7d7f 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -5,26 +5,36 @@ from dataclasses import dataclass, field from typing import Any, Callable, Sequence, TypedDict, TypeVar +from reactpy.backend.types import Location +from reactpy.core.component import Component from reactpy.core.vdom import is_vdom -from reactpy.types import ComponentType, Key +from reactpy.types import Key from typing_extensions import Protocol, Self, TypeAlias ConversionFunc: TypeAlias = Callable[[str], Any] +"""A function that converts a string to a specific type.""" + ConverterMapping: TypeAlias = dict[str, ConversionFunc] +"""A mapping of conversion types to their respective functions.""" @dataclass(frozen=True) class Route: - """A route that can be matched against a path.""" + """ + A class representing a route that can be matched against a path. - path: str - """The path to match against.""" + Attributes: + path (str): The path to match against. + element (Any): The element to render if the path matches. + routes (Sequence[Self]): Child routes. - element: Any = field(hash=False) - """The element to render if the path matches.""" + Methods: + __hash__() -> int: Returns a hash value for the route based on its path, element, and child routes. + """ + path: str + element: Any = field(hash=False) routes: Sequence[Self] - """Child routes.""" def __hash__(self) -> int: el = self.element @@ -33,36 +43,87 @@ def __hash__(self) -> int: RouteType = TypeVar("RouteType", bound=Route) +"""A type variable for `Route`.""" + RouteType_contra = TypeVar("RouteType_contra", bound=Route, contravariant=True) +"""A contravariant type variable for `Route`.""" class Router(Protocol[RouteType_contra]): - """Return a component that renders the first matching route.""" + """Return a component that renders the matching route(s).""" + + def __call__(self, *routes: RouteType_contra) -> Component: + """ + Process the given routes and return a component that renders the matching route(s). - def __call__(self, *routes: RouteType_contra) -> ComponentType: ... + Args: + *routes: A variable number of route arguments. + + Returns: + The resulting component after processing the routes. + """ class Resolver(Protocol[RouteType_contra]): """Compile a route into a resolver that can be matched against a given path.""" - def __call__(self, route: RouteType_contra) -> CompiledRoute: ... + def __call__(self, route: RouteType_contra) -> CompiledRoute: + """ + Compile a route into a resolver that can be matched against a given path. + + Args: + route: The route to compile. + + Returns: + The compiled route. + """ class CompiledRoute(Protocol): - """A compiled route that can be matched against a path.""" + """ + A protocol for a compiled route that can be matched against a path. + + Attributes: + key (Key): A property that uniquely identifies this resolver. + """ @property - def key(self) -> Key: - """Uniquely identified this resolver.""" + def key(self) -> Key: ... def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: - """Return the path's associated element and path parameters or None.""" + """ + Return the path's associated element and path parameters or None. + + Args: + path (str): The path to resolve. + + Returns: + A tuple containing the associated element and a dictionary of path parameters, or None if the path cannot be resolved. + """ class ConversionInfo(TypedDict): - """Information about a conversion type.""" + """ + A TypedDict that holds information about a conversion type. + + Attributes: + regex (str): The regex to match the conversion type. + func (ConversionFunc): The function to convert the matched string to the expected type. + """ regex: str - """The regex to match the conversion type.""" func: ConversionFunc - """The function to convert the matched string to the expected type.""" + + +@dataclass +class RouteState: + """ + Represents the state of a route in the application. + + Attributes: + set_location: A callable to set the location. + params: A dictionary containing route parameters. + """ + + set_location: Callable[[Location], None] + params: dict[str, Any] diff --git a/tests/conftest.py b/tests/conftest.py index 18e3646..7d6f0ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,4 @@ -import asyncio import os -import sys import pytest from playwright.async_api import async_playwright @@ -18,27 +16,25 @@ def pytest_addoption(parser) -> None: ) -@pytest.fixture +@pytest.fixture(scope="session") async def display(backend, browser): async with DisplayFixture(backend, browser) as display_fixture: display_fixture.page.set_default_timeout(10000) yield display_fixture -@pytest.fixture +@pytest.fixture(scope="session") async def backend(): async with BackendFixture() as backend_fixture: yield backend_fixture -@pytest.fixture +@pytest.fixture(scope="session") async def browser(pytestconfig): async with async_playwright() as pw: yield await pw.chromium.launch(headless=True if GITHUB_ACTIONS else pytestconfig.getoption("headless")) -@pytest.fixture -def event_loop_policy(request): - if sys.platform == "win32": - return asyncio.WindowsProactorEventLoopPolicy() - return asyncio.get_event_loop_policy() +@pytest.fixture(scope="session") +def anyio_backend(): + return "asyncio" diff --git a/tests/test_router.py b/tests/test_router.py index d6e0deb..1a4d95c 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -1,14 +1,17 @@ +import asyncio import os from typing import Any +import pytest from playwright.async_api._generated import Browser, Page -from reactpy import Ref, component, html, use_location +from reactpy import Ref, component, html, use_location, use_state from reactpy.testing import DisplayFixture -from reactpy_router import browser_router, link, route, use_params, use_search_params +from reactpy_router import browser_router, link, navigate, route, use_params, use_search_params GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "").lower() == "true" -CLICK_DELAY = 350 if GITHUB_ACTIONS else 25 # Delay in miliseconds. +CLICK_DELAY = 250 if GITHUB_ACTIONS else 25 # Delay in miliseconds. +pytestmark = pytest.mark.anyio async def test_simple_router(display: DisplayFixture): @@ -174,23 +177,19 @@ def sample(): await display.show(sample) - for link_selector in ["#root", "#a", "#b", "#c"]: + link_selectors = ["#root", "#a", "#b", "#c"] + + for link_selector in link_selectors: _link = await display.page.wait_for_selector(link_selector) await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") - await display.page.go_back() - await display.page.wait_for_selector("#c") - - await display.page.go_back() - await display.page.wait_for_selector("#b") - - await display.page.go_back() - await display.page.wait_for_selector("#a") - - await display.page.go_back() - await display.page.wait_for_selector("#root") + link_selectors.reverse() + for link_selector in link_selectors: + await asyncio.sleep(CLICK_DELAY / 1000) + await display.page.go_back() + await display.page.wait_for_selector(link_selector) async def test_relative_links(display: DisplayFixture): @@ -209,32 +208,19 @@ def sample(): await display.show(sample) - for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e", "#f"]: + selectors = ["#root", "#a", "#b", "#c", "#d", "#e", "#f"] + + for link_selector in selectors: _link = await display.page.wait_for_selector(link_selector) await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") - await display.page.go_back() - await display.page.wait_for_selector("#f") - - await display.page.go_back() - await display.page.wait_for_selector("#e") - - await display.page.go_back() - await display.page.wait_for_selector("#d") - - await display.page.go_back() - await display.page.wait_for_selector("#c") - - await display.page.go_back() - await display.page.wait_for_selector("#b") - - await display.page.go_back() - await display.page.wait_for_selector("#a") - - await display.page.go_back() - await display.page.wait_for_selector("#root") + selectors.reverse() + for link_selector in selectors: + await asyncio.sleep(CLICK_DELAY / 1000) + await display.page.go_back() + await display.page.wait_for_selector(link_selector) async def test_link_with_query_string(display: DisplayFixture): @@ -293,5 +279,92 @@ def sample(): _link = await display.page.wait_for_selector("#root") await _link.click(delay=CLICK_DELAY, modifiers=["Control"]) browser_context = browser.contexts[0] - new_page: Page = await browser_context.wait_for_event("page") + if len(browser_context.pages) == 1: + new_page: Page = await browser_context.wait_for_event("page") + else: + new_page: Page = browser_context.pages[-1] # type: ignore[no-redef] await new_page.wait_for_selector("#a") + + +async def test_navigate_component(display: DisplayFixture): + @component + def navigate_btn(): + nav_url, set_nav_url = use_state("") + + return html.button( + {"onClick": lambda _: set_nav_url("/a")}, + navigate(nav_url) if nav_url else "Click to navigate", + ) + + @component + def sample(): + return browser_router( + route("/", navigate_btn()), + route("/a", html.h1({"id": "a"}, "A")), + ) + + await display.show(sample) + _button = await display.page.wait_for_selector("button") + await _button.click(delay=CLICK_DELAY) + await display.page.wait_for_selector("#a") + await asyncio.sleep(CLICK_DELAY / 1000) + await display.page.go_back() + await display.page.wait_for_selector("button") + + +async def test_navigate_component_replace(display: DisplayFixture): + @component + def navigate_btn(to: str, replace: bool = False): + nav_url, set_nav_url = use_state("") + + return html.button( + {"onClick": lambda _: set_nav_url(to), "id": f"nav-{to.replace('/', '')}"}, + navigate(nav_url, replace) if nav_url else f"Navigate to {to}", + ) + + @component + def sample(): + return browser_router( + route("/", navigate_btn("/a")), + route("/a", navigate_btn("/b", replace=True)), + route("/b", html.h1({"id": "b"}, "B")), + ) + + await display.show(sample) + _button = await display.page.wait_for_selector("#nav-a") + await _button.click(delay=CLICK_DELAY) + _button = await display.page.wait_for_selector("#nav-b") + await _button.click(delay=CLICK_DELAY) + await display.page.wait_for_selector("#b") + await asyncio.sleep(CLICK_DELAY / 1000) + await display.page.go_back() + await display.page.wait_for_selector("#nav-a") + + +async def test_navigate_component_to_current_url(display: DisplayFixture): + @component + def navigate_btn(to: str, html_id: str): + nav_url, set_nav_url = use_state("") + + return html.button( + {"onClick": lambda _: set_nav_url(to), "id": html_id}, + navigate(nav_url) if nav_url else f"Navigate to {to}", + ) + + @component + def sample(): + return browser_router( + route("/", navigate_btn("/a", "root-a")), + route("/a", navigate_btn("/a", "nav-a")), + ) + + await display.show(sample) + _button = await display.page.wait_for_selector("#root-a") + await _button.click(delay=CLICK_DELAY) + _button = await display.page.wait_for_selector("#nav-a") + await _button.click(delay=CLICK_DELAY) + await asyncio.sleep(CLICK_DELAY / 1000) + await display.page.wait_for_selector("#nav-a") + await asyncio.sleep(CLICK_DELAY / 1000) + await display.page.go_back() + await display.page.wait_for_selector("#root-a") From 80a48f0d5b1ba1b2a68360fbea1b94252db57856 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Fri, 18 Oct 2024 00:24:23 -0700 Subject: [PATCH 09/22] v1.0.0 (#36) --- CHANGELOG.md | 7 ++++++- src/reactpy_router/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4e08c..a255a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ Using the following categories, list your changes in this order: ## [Unreleased] +- Nothing (yet)! + +## [1.0.0] - 2024-10-18 + ### Changed - Rename `use_query` to `use_search_params`. @@ -87,7 +91,8 @@ Using the following categories, list your changes in this order: - Rename `configure` to `create_router`. - Rename from `idom-router` to `reactpy-router`. -[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.0...HEAD +[1.0.0]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...1.0.0 [0.1.1]: https://github.com/reactive-python/reactpy-router/compare/0.1.0...0.1.1 [0.1.0]: https://github.com/reactive-python/reactpy-router/compare/0.0.1...0.1.0 [0.0.1]: https://github.com/reactive-python/reactpy-router/releases/tag/0.0.1 diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index 9f272c2..cd07f57 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -1,5 +1,5 @@ # the version is statically loaded by setup.py -__version__ = "0.1.1" +__version__ = "1.0.0" from .components import link, navigate, route From 3aecbafe911b0fd41b847179173c6e6a59cda7b4 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Fri, 18 Oct 2024 01:12:43 -0700 Subject: [PATCH 10/22] Fix python publishing workflow (#37) --- noxfile.py | 1 + requirements/build-pkg.txt | 1 + requirements/test-env.txt | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 6376072..670ac44 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,6 +39,7 @@ def test_style(session: Session) -> None: @session(tags=["test"]) def test_javascript(session: Session) -> None: install_requirements_file(session, "test-env") + install_requirements_file(session, "build-pkg") session.chdir(ROOT_DIR / "src" / "js") session.run("python", "-m", "nodejs.npm", "install", external=True) session.run("python", "-m", "nodejs.npm", "run", "check") diff --git a/requirements/build-pkg.txt b/requirements/build-pkg.txt index 88ec271..cd3dc5a 100644 --- a/requirements/build-pkg.txt +++ b/requirements/build-pkg.txt @@ -1,3 +1,4 @@ twine wheel setuptools +nodejs-bin==18.4.0a4 diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 4b78ca5..0681f8b 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -3,4 +3,3 @@ pytest anyio pytest-cov reactpy[testing,starlette] -nodejs-bin==18.4.0a4 From bcb310f0b8f011e068c66e1286d1b25dba3b00a4 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Fri, 18 Oct 2024 21:07:51 -0700 Subject: [PATCH 11/22] Switch to TypeScript (#40) --- README.md | 4 +- docs/src/index.md | 10 +- docs/src/learn/your-first-app.md | 2 +- setup.cfg | 3 - setup.py | 1 + src/js/package-lock.json | 1170 ++++++++++++++++++++++-------- src/js/package.json | 31 +- src/js/rollup.config.js | 25 - src/js/rollup.config.mjs | 23 + src/js/src/index.js | 150 ---- src/js/src/index.ts | 129 ++++ src/js/src/types.ts | 23 + src/js/src/utils.ts | 16 + src/js/tsconfig.json | 9 + 14 files changed, 1087 insertions(+), 509 deletions(-) delete mode 100644 src/js/rollup.config.js create mode 100644 src/js/rollup.config.mjs delete mode 100644 src/js/src/index.js create mode 100644 src/js/src/index.ts create mode 100644 src/js/src/types.ts create mode 100644 src/js/src/utils.ts create mode 100644 src/js/tsconfig.json diff --git a/README.md b/README.md index 3cb0c5d..a868aac 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ReactPy Router

- - + + diff --git a/docs/src/index.md b/docs/src/index.md index 3630a87..969d43c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,6 +6,12 @@ Run the following command to install [`reactpy-router`](https://pypi.org/project pip install reactpy-router ``` -## Done! +## Quick Start -You're now ready to start building your own ReactPy applications with URL routing. +You're now ready to start building your own ReactPy applications with URL routing. For example... + +=== "components.py" + + ```python + {% include "../examples/python/basic-routing.py" %} + ``` diff --git a/docs/src/learn/your-first-app.md b/docs/src/learn/your-first-app.md index 4b67677..8ffb3d7 100644 --- a/docs/src/learn/your-first-app.md +++ b/docs/src/learn/your-first-app.md @@ -6,7 +6,7 @@ Here you'll learn the various features of `reactpy-router` and how to use them. !!! abstract "Note" - These docs assume you already know the basics of [ReacPy](https://reactpy.dev). + These docs assume you already know the basics of [ReactPy](https://reactpy.dev). --- diff --git a/setup.cfg b/setup.cfg index e7be17a..826acaa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal=1 - [coverage:report] fail_under = 100 show_missing = True diff --git a/setup.py b/setup.py index c643fa1..2bf91bc 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", "Typing :: Typed", diff --git a/src/js/package-lock.json b/src/js/package-lock.json index c98d3c6..b1a7859 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -6,17 +6,21 @@ "": { "name": "reactpy-router", "dependencies": { - "react": "^17.0.1", - "react-dom": "^17.0.1" + "@rollup/plugin-typescript": "^12.1.1", + "preact": "^10.24.3", + "tslib": "^2.8.0" }, "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", - "prettier": "^2.2.1", - "rollup": "^2.35.1", - "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0" + "prettier": "^3.3.3", + "rollup": "^4.24.0", + "typescript": "^5.6.3" } }, "node_modules/@eslint-community/eslint-utils": { @@ -108,6 +112,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "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", @@ -143,27 +153,378 @@ "node": ">= 8" } }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "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": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", + "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": "12.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", + "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "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/pluginutils/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/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, - "node_modules/@types/node": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.0.tgz", - "integrity": "sha512-OyiZPohMMjZEYqcVo/UJ04GyAxXOJEZO/FpzyXxcH4r/ArrVoXHf4MbUrkLp0Tz7/p1mMKpo5zJ6ZHl8XBNthQ==", + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "dev": true }, - "node_modules/@types/resolve": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", - "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "node_modules/@types/react": { + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", "dev": true, "dependencies": { - "@types/node": "*" + "@types/prop-types": "*", + "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "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/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -322,18 +683,6 @@ "concat-map": "0.0.1" } }, - "node_modules/builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -390,6 +739,12 @@ "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", @@ -410,6 +765,12 @@ "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", @@ -433,6 +794,15 @@ "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-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -755,10 +1125,9 @@ } }, "node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true + "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", @@ -796,6 +1165,20 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -873,10 +1256,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "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.5", @@ -1097,6 +1482,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1216,12 +1612,14 @@ } }, "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dev": true, + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1266,7 +1664,7 @@ "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true }, "node_modules/is-negative-zero": { @@ -1422,7 +1820,8 @@ "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==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -1499,6 +1898,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -1507,12 +1907,12 @@ } }, "node_modules/magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "dependencies": { - "sourcemap-codec": "^1.4.4" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/minimatch": { @@ -1543,6 +1943,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1742,8 +2143,28 @@ "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==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -1755,15 +2176,18 @@ } }, "node_modules/prettier": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", - "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prop-types": { @@ -1806,31 +2230,6 @@ } ] }, - "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==", - "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==", - "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", @@ -1855,13 +2254,16 @@ } }, "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, + "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.2.0", - "path-parse": "^1.0.6" + "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" @@ -1902,74 +2304,40 @@ } }, "node_modules/rollup": { - "version": "2.56.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.2.tgz", - "integrity": "sha512-s8H00ZsRi29M2/lGdm1u8DJpJ9ML8SUOpVVBd33XNeEeL3NVaTiUcSBHzBdF3eAyR0l7VSpsuoVUGrRHq7aPwQ==", - "dev": true, + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "devOptional": true, + "dependencies": { + "@types/estree": "1.0.6" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-commonjs": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", - "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1", - "is-reference": "^1.1.2", - "magic-string": "^0.25.2", - "resolve": "^1.11.0", - "rollup-pluginutils": "^2.8.1" - }, - "peerDependencies": { - "rollup": ">=1.12.0" - } - }, - "node_modules/rollup-plugin-node-resolve": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", - "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", - "dev": true, - "dependencies": { - "@types/resolve": "0.0.8", - "builtin-modules": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.11.1", - "rollup-pluginutils": "^2.8.1" - }, - "peerDependencies": { - "rollup": ">=1.11.0" - } - }, - "node_modules/rollup-plugin-replace": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", - "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", - "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.", - "dev": true, - "dependencies": { - "magic-string": "^0.25.2", - "rollup-pluginutils": "^2.6.0" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2007,19 +2375,10 @@ "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==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "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" @@ -2060,12 +2419,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -2170,7 +2523,6 @@ "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==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2184,6 +2536,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2222,6 +2579,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "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", @@ -2298,9 +2667,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -2387,6 +2756,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2413,27 +2788,218 @@ "fastq": "^1.6.0" } }, + "@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + } + }, + "@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + } + }, + "@rollup/plugin-replace": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + } + }, + "@rollup/plugin-typescript": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", + "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", + "requires": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + } + }, + "@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + } + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "dev": true, + "optional": true + }, "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, - "@types/node": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.0.tgz", - "integrity": "sha512-OyiZPohMMjZEYqcVo/UJ04GyAxXOJEZO/FpzyXxcH4r/ArrVoXHf4MbUrkLp0Tz7/p1mMKpo5zJ6ZHl8XBNthQ==", + "@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "dev": true }, - "@types/resolve": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", - "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "@types/react": { + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "requires": { - "@types/node": "*" + "@types/react": "*" } }, + "@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 + }, "acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -2550,12 +3116,6 @@ "concat-map": "0.0.1" } }, - "builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2597,6 +3157,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2614,6 +3180,12 @@ "which": "^2.0.1" } }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2629,6 +3201,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "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 + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -2872,10 +3450,9 @@ "dev": true }, "estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true + "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==" }, "esutils": { "version": "2.0.3", @@ -2910,6 +3487,13 @@ "reusify": "^1.0.4" } }, + "fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "requires": {} + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2968,10 +3552,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -3120,6 +3703,14 @@ "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3206,12 +3797,11 @@ "dev": true }, "is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dev": true, + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "requires": { - "has": "^1.0.3" + "hasown": "^2.0.2" } }, "is-date-object": { @@ -3241,7 +3831,7 @@ "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true }, "is-negative-zero": { @@ -3348,7 +3938,8 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "4.1.0", @@ -3410,17 +4001,18 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } }, "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "requires": { - "sourcemap-codec": "^1.4.4" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "minimatch": { @@ -3447,7 +4039,8 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true }, "object-inspect": { "version": "1.12.3", @@ -3587,9 +4180,19 @@ "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==", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true }, + "preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3597,9 +4200,9 @@ "dev": true }, "prettier": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", - "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true }, "prop-types": { @@ -3625,25 +4228,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - } - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3662,13 +4246,13 @@ } }, "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "resolve-from": { @@ -3693,59 +4277,31 @@ } }, "rollup": { - "version": "2.56.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.2.tgz", - "integrity": "sha512-s8H00ZsRi29M2/lGdm1u8DJpJ9ML8SUOpVVBd33XNeEeL3NVaTiUcSBHzBdF3eAyR0l7VSpsuoVUGrRHq7aPwQ==", - "dev": true, + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "devOptional": true, "requires": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@types/estree": "1.0.6", "fsevents": "~2.3.2" } }, - "rollup-plugin-commonjs": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", - "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1", - "is-reference": "^1.1.2", - "magic-string": "^0.25.2", - "resolve": "^1.11.0", - "rollup-pluginutils": "^2.8.1" - } - }, - "rollup-plugin-node-resolve": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", - "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", - "dev": true, - "requires": { - "@types/resolve": "0.0.8", - "builtin-modules": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.11.1", - "rollup-pluginutils": "^2.8.1" - } - }, - "rollup-plugin-replace": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", - "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", - "dev": true, - "requires": { - "magic-string": "^0.25.2", - "rollup-pluginutils": "^2.6.0" - } - }, - "rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3766,19 +4322,10 @@ "is-regex": "^1.1.4" } }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "shebang-command": { @@ -3807,12 +4354,6 @@ "object-inspect": "^1.9.0" } }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, "string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -3889,8 +4430,7 @@ "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==", - "dev": true + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "text-table": { "version": "0.2.0", @@ -3898,6 +4438,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3924,6 +4469,11 @@ "is-typed-array": "^1.1.9" } }, + "typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -3982,9 +4532,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrappy": { diff --git a/src/js/package.json b/src/js/package.json index d4ac92c..b9370a4 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,33 +1,32 @@ { "name": "reactpy-router", "description": "A URL router for ReactPy", - "author": "Ryan Morshead", + "author": "Mark Bakhit", "repository": { "type": "git", "url": "https://github.com/reactive-python/reactpy-router" }, - "main": "src/index.js", - "files": [ - "src/**/*.js" - ], + "main": "src/index.tsx", "scripts": { "build": "rollup --config", - "format": "prettier --write . && eslint --fix .", - "check": "npm run check:format", - "check:format": "prettier --check . && eslint .", - "test": "echo \"Error: no test specified\" && exit 1" + "format": "prettier --write . && eslint --fix", + "check": "prettier --check . && eslint" }, "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", - "prettier": "^2.2.1", - "rollup": "^2.35.1", - "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0" + "prettier": "^3.3.3", + "rollup": "^4.24.0", + "typescript": "^5.6.3" }, "dependencies": { - "react": "^17.0.1", - "react-dom": "^17.0.1" + "@rollup/plugin-typescript": "^12.1.1", + "preact": "^10.24.3", + "tslib": "^2.8.0" } } diff --git a/src/js/rollup.config.js b/src/js/rollup.config.js deleted file mode 100644 index 396fd87..0000000 --- a/src/js/rollup.config.js +++ /dev/null @@ -1,25 +0,0 @@ -import resolve from "rollup-plugin-node-resolve"; -import commonjs from "rollup-plugin-commonjs"; -import replace from "rollup-plugin-replace"; - -export default { - input: "src/index.js", - output: { - file: "../reactpy_router/static/bundle.js", - format: "esm", - }, - plugins: [ - resolve(), - commonjs(), - replace({ - "process.env.NODE_ENV": JSON.stringify("production"), - }), - ], - onwarn: function (warning) { - if (warning.code === "THIS_IS_UNDEFINED") { - // skip warning where `this` is undefined at the top level of a module - return; - } - console.warn(warning.message); - }, -}; diff --git a/src/js/rollup.config.mjs b/src/js/rollup.config.mjs new file mode 100644 index 0000000..0410a35 --- /dev/null +++ b/src/js/rollup.config.mjs @@ -0,0 +1,23 @@ +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.ts", + output: { + file: "../reactpy_router/static/bundle.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/index.js b/src/js/src/index.js deleted file mode 100644 index 4e7f02f..0000000 --- a/src/js/src/index.js +++ /dev/null @@ -1,150 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; - -export function bind(node) { - return { - create: (type, props, children) => - React.createElement(type, props, ...children), - render: (element) => { - ReactDOM.render(element, node); - }, - unmount: () => ReactDOM.unmountComponentAtNode(node), - }; -} - -/** - * History component that captures browser "history go back" actions and notifies the server. - * - * @param {Object} props - The properties object. - * @param {Function} props.onHistoryChangeCallback - Callback function to notify the server about history changes. - * @returns {null} This component does not render any visible output. - * @description - * This component uses the `popstate` event to detect when the user navigates back in the browser history. - * It then calls the `onHistoryChangeCallback` with the current pathname and search parameters. - * Note: Browsers do not allow detection of "history go forward" actions. - * @see https://github.com/reactive-python/reactpy/pull/1224 - */ -export function History({ onHistoryChangeCallback }) { - React.useEffect(() => { - // Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback. - const listener = () => { - onHistoryChangeCallback({ - pathname: window.location.pathname, - search: window.location.search, - }); - }; - - // Register the event listener - window.addEventListener("popstate", listener); - - // Delete the event listener when the component is unmounted - return () => window.removeEventListener("popstate", listener); - }); - - // Tell the server about the URL during the initial page load - // FIXME: This code is commented out since it currently runs every time any component - // is mounted due to a ReactPy core rendering bug. `FirstLoad` component is used instead. - // https://github.com/reactive-python/reactpy/pull/1224 - - // React.useEffect(() => { - // onHistoryChange({ - // pathname: window.location.pathname, - // search: window.location.search, - // }); - // return () => {}; - // }, []); - return null; -} - -/** - * Link component that captures clicks on anchor links and notifies the server. - * - * @param {Object} props - The properties object. - * @param {Function} props.onClickCallback - Callback function to notify the server about link clicks. - * @param {string} props.linkClass - The class name of the anchor link. - * @returns {null} This component does not render any visible output. - */ -export function Link({ onClickCallback, linkClass }) { - // FIXME: This component is currently unused due to a ReactPy core rendering bug - // which causes duplicate rendering (and thus duplicate event listeners). - // https://github.com/reactive-python/reactpy/pull/1224 - - // This component is not the actual anchor link. - // It is an event listener for the link component created by ReactPy. - React.useEffect(() => { - // Event function that will tell the server about clicks - const handleClick = (event) => { - event.preventDefault(); - let to = event.target.getAttribute("href"); - window.history.pushState(null, "", new URL(to, window.location)); - onClickCallback({ - pathname: window.location.pathname, - search: window.location.search, - }); - }; - - // Register the event listener - let link = document.querySelector(`.${linkClass}`); - if (link) { - link.addEventListener("click", handleClick); - } - - // Delete the event listener when the component is unmounted - return () => { - let link = document.querySelector(`.${linkClass}`); - if (link) { - link.removeEventListener("click", handleClick); - } - }; - }); - return null; -} - -/** - * Client-side portion of the navigate component, that allows the server to command the client to change URLs. - * - * @param {Object} props - The properties object. - * @param {Function} props.onNavigateCallback - Callback function that transmits data to the server. - * @param {string} props.to - The target URL to navigate to. - * @param {boolean} props.replace - If true, replaces the current history entry instead of adding a new one. - * @returns {null} This component does not render anything. - */ -export function Navigate({ onNavigateCallback, to, replace }) { - React.useEffect(() => { - if (replace) { - window.history.replaceState(null, "", new URL(to, window.location)); - } else { - window.history.pushState(null, "", new URL(to, window.location)); - } - onNavigateCallback({ - pathname: window.location.pathname, - search: window.location.search, - }); - return () => {}; - }, []); - - return null; -} - -/** - * FirstLoad component that captures the URL during the initial page load and notifies the server. - * - * @param {Object} props - The properties object. - * @param {Function} props.onFirstLoadCallback - Callback function to notify the server about the first load. - * @returns {null} This component does not render any visible output. - * @description - * This component sends the current URL to the server during the initial page load. - * @see https://github.com/reactive-python/reactpy/pull/1224 - */ -export function FirstLoad({ onFirstLoadCallback }) { - // FIXME: This component only exists because of a ReactPy core rendering bug, and should be removed when the bug - // is fixed. Ideally all this logic would be handled by the `History` component. - React.useEffect(() => { - onFirstLoadCallback({ - pathname: window.location.pathname, - search: window.location.search, - }); - return () => {}; - }, []); - return null; -} diff --git a/src/js/src/index.ts b/src/js/src/index.ts new file mode 100644 index 0000000..d7c6b3e --- /dev/null +++ b/src/js/src/index.ts @@ -0,0 +1,129 @@ +import React from "preact/compat"; +import ReactDOM from "preact/compat"; +import { createLocationObject, pushState, replaceState } from "./utils"; +import { + HistoryProps, + LinkProps, + NavigateProps, + FirstLoadProps, +} from "./types"; + +/** + * 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), + }; +} + +/** + * History component that captures browser "history go back" actions and notifies the server. + */ +export function History({ onHistoryChangeCallback }: HistoryProps): null { + React.useEffect(() => { + // Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback. + const listener = () => { + onHistoryChangeCallback(createLocationObject()); + }; + + // Register the event listener + window.addEventListener("popstate", listener); + + // Delete the event listener when the component is unmounted + return () => window.removeEventListener("popstate", listener); + }); + + // Tell the server about the URL during the initial page load + // FIXME: This code is commented out since it currently runs every time any component + // is mounted due to a ReactPy core rendering bug. `FirstLoad` component is used instead. + // https://github.com/reactive-python/reactpy/pull/1224 + // React.useEffect(() => { + // onHistoryChange({ + // pathname: window.location.pathname, + // search: window.location.search, + // }); + // return () => {}; + // }, []); + return null; +} + +/** + * Link component that captures clicks on anchor links and notifies the server. + * + * This component is not the actual `` link element. It is just an event + * listener for ReactPy-Router's server-side link component. + * + * @disabled This component is currently unused due to a ReactPy core rendering bug + * which causes duplicate rendering (and thus duplicate event listeners). + */ +export function Link({ onClickCallback, linkClass }: LinkProps): null { + React.useEffect(() => { + // Event function that will tell the server about clicks + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + let to = (event.target as HTMLElement).getAttribute("href"); + pushState(to); + onClickCallback(createLocationObject()); + }; + + // Register the event listener + let link = document.querySelector(`.${linkClass}`); + if (link) { + link.addEventListener("click", handleClick); + } else { + console.warn(`Link component with class name ${linkClass} not found.`); + } + + // Delete the event listener when the component is unmounted + return () => { + let link = document.querySelector(`.${linkClass}`); + if (link) { + link.removeEventListener("click", handleClick); + } + }; + }); + return null; +} + +/** + * Client-side portion of the navigate component, that allows the server to command the client to change URLs. + */ +export function Navigate({ + onNavigateCallback, + to, + replace = false, +}: NavigateProps): null { + React.useEffect(() => { + if (replace) { + replaceState(to); + } else { + pushState(to); + } + onNavigateCallback(createLocationObject()); + return () => {}; + }, []); + + return null; +} + +/** + * FirstLoad component that captures the URL during the initial page load and notifies the server. + * + * FIXME: This component only exists because of a ReactPy core rendering bug, and should be removed when the bug + * is fixed. In the future, all this logic should be handled by the `History` component. + * https://github.com/reactive-python/reactpy/pull/1224 + */ +export function FirstLoad({ onFirstLoadCallback }: FirstLoadProps): null { + React.useEffect(() => { + onFirstLoadCallback(createLocationObject()); + return () => {}; + }, []); + + return null; +} diff --git a/src/js/src/types.ts b/src/js/src/types.ts new file mode 100644 index 0000000..f4cf6cd --- /dev/null +++ b/src/js/src/types.ts @@ -0,0 +1,23 @@ +export interface ReactPyLocation { + pathname: string; + search: string; +} + +export interface HistoryProps { + onHistoryChangeCallback: (location: ReactPyLocation) => void; +} + +export interface LinkProps { + onClickCallback: (location: ReactPyLocation) => void; + linkClass: string; +} + +export interface NavigateProps { + onNavigateCallback: (location: ReactPyLocation) => void; + to: string; + replace?: boolean; +} + +export interface FirstLoadProps { + onFirstLoadCallback: (location: ReactPyLocation) => void; +} diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts new file mode 100644 index 0000000..a0d1af7 --- /dev/null +++ b/src/js/src/utils.ts @@ -0,0 +1,16 @@ +import { ReactPyLocation } from "./types"; + +export function createLocationObject(): ReactPyLocation { + return { + pathname: window.location.pathname, + search: window.location.search, + }; +} + +export function pushState(to: string): void { + window.history.pushState(null, "", new URL(to, window.location.href)); +} + +export function replaceState(to: string): void { + window.history.replaceState(null, "", new URL(to, window.location.href)); +} diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json new file mode 100644 index 0000000..2060912 --- /dev/null +++ b/src/js/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "allowSyntheticDefaultImports": true + } +} From da15d41d53184950facefd88a60f3edd6b99b224 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 24 Oct 2024 01:20:47 -0700 Subject: [PATCH 12/22] Rework CI to utilize `Bun` and `Hatch` (#41) - JavaScript bundle is now created using [`Bun`](https://bun.sh/) - Python package is now built using [`Hatch`](https://hatch.pypa.io/) --- .editorconfig | 2 + .github/workflows/publish-develop-docs.yml | 47 +- .github/workflows/publish-latest-docs.yml | 31 + .github/workflows/publish-py.yaml | 47 +- .github/workflows/publish-release-docs.yml | 23 - .github/workflows/test-docs.yml | 15 +- .github/workflows/test-src.yaml | 40 - .github/workflows/test-src.yml | 68 + .github/workflows/test-style.yml | 27 + CHANGELOG.md | 5 +- MANIFEST.in | 2 - .../{basic-routing.py => basic_routing.py} | 0 ...routes.py => basic_routing_more_routes.py} | 0 .../{nested-routes.py => nested_routes.py} | 19 +- .../python/{route-links.py => route_links.py} | 0 ...oute-parameters.py => route_parameters.py} | 19 +- .../python/{use-params.py => use_params.py} | 0 ...-search-params.py => use_search_params.py} | 0 docs/src/index.md | 2 +- docs/src/learn/hooks.md | 4 +- docs/src/learn/routers-routes-and-links.md | 4 +- docs/src/learn/your-first-app.md | 10 +- noxfile.py | 52 - pyproject.toml | 134 +- requirements.txt | 7 - requirements/build-docs.txt | 13 - requirements/build-pkg.txt | 4 - requirements/check-style.txt | 1 - requirements/check-types.txt | 2 - requirements/pkg-deps.txt | 2 - requirements/test-env.txt | 5 - requirements/test-run.txt | 1 - setup.cfg | 9 - setup.py | 131 - src/js/.eslintrc.json | 22 - src/js/bun.lockb | Bin 0 -> 106219 bytes src/js/eslint.config.mjs | 43 + src/js/package-lock.json | 4553 ----------------- src/js/package.json | 24 +- src/js/rollup.config.mjs | 23 - src/js/tsconfig.json | 9 - src/reactpy_router/__init__.py | 10 +- src/reactpy_router/components.py | 19 +- src/reactpy_router/hooks.py | 12 +- src/reactpy_router/resolvers.py | 9 +- src/reactpy_router/routers.py | 11 +- src/reactpy_router/types.py | 12 +- tests/conftest.py | 7 + tests/test_resolver.py | 2 +- tests/test_router.py | 3 +- 50 files changed, 445 insertions(+), 5040 deletions(-) create mode 100644 .github/workflows/publish-latest-docs.yml delete mode 100644 .github/workflows/publish-release-docs.yml delete mode 100644 .github/workflows/test-src.yaml create mode 100644 .github/workflows/test-src.yml create mode 100644 .github/workflows/test-style.yml delete mode 100644 MANIFEST.in rename docs/examples/python/{basic-routing.py => basic_routing.py} (100%) rename docs/examples/python/{basic-routing-more-routes.py => basic_routing_more_routes.py} (100%) rename docs/examples/python/{nested-routes.py => nested_routes.py} (89%) rename docs/examples/python/{route-links.py => route_links.py} (100%) rename docs/examples/python/{route-parameters.py => route_parameters.py} (88%) rename docs/examples/python/{use-params.py => use_params.py} (100%) rename docs/examples/python/{use-search-params.py => use_search_params.py} (100%) delete mode 100644 noxfile.py delete mode 100644 requirements.txt delete mode 100644 requirements/build-docs.txt delete mode 100644 requirements/build-pkg.txt delete mode 100644 requirements/check-style.txt delete mode 100644 requirements/check-types.txt delete mode 100644 requirements/pkg-deps.txt delete mode 100644 requirements/test-env.txt delete mode 100644 requirements/test-run.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 src/js/.eslintrc.json create mode 100644 src/js/bun.lockb create mode 100644 src/js/eslint.config.mjs delete mode 100644 src/js/package-lock.json delete mode 100644 src/js/rollup.config.mjs delete mode 100644 src/js/tsconfig.json diff --git a/.editorconfig b/.editorconfig index 356385d..f066b28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,10 +14,12 @@ end_of_line = lf indent_size = 4 max_line_length = 120 + [*.md] indent_size = 4 [*.html] +indent_size = 4 max_line_length = off [*.js] diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index 95d98da..65a1bbb 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -1,22 +1,31 @@ name: Publish Develop Docs on: - push: - branches: - - main + push: + branches: + - main jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - run: pip install -r requirements/build-docs.txt - - name: Publish Develop Docs - run: | - git config user.name github-actions - git config user.email github-actions@github.com - cd docs - mike deploy --push develop + deploy: + 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: Install dependecies + run: | + pip install --upgrade hatch uv + - name: Configure Git + run: | + git config user.name github-actions + git config user.email github-actions@github.com + - name: Publish Develop Docs + run: | + hatch run docs:deploy_develop + concurrency: + group: publish-docs diff --git a/.github/workflows/publish-latest-docs.yml b/.github/workflows/publish-latest-docs.yml new file mode 100644 index 0000000..7ce0c00 --- /dev/null +++ b/.github/workflows/publish-latest-docs.yml @@ -0,0 +1,31 @@ +name: Publish Latest Docs +on: + release: + types: [published] + +jobs: + deploy: + 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: Install dependecies + run: | + pip install --upgrade hatch uv + - name: Configure Git + run: | + git config user.name github-actions + git config user.email github-actions@github.com + - name: Publish Develop Docs + run: | + hatch run docs:deploy_latest ${{ github.ref_name }} + concurrency: + group: publish-docs diff --git a/.github/workflows/publish-py.yaml b/.github/workflows/publish-py.yaml index d7437d7..430a3f0 100644 --- a/.github/workflows/publish-py.yaml +++ b/.github/workflows/publish-py.yaml @@ -4,26 +4,31 @@ name: Publish Python on: - release: - types: [published] + release: + types: [published] jobs: - publish-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 setup.py bdist_wheel - twine upload dist/* + publish-package: + 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: | + pip3 --quiet install --upgrade hatch uv twine + pip install -r requirements/build-pkg.txt + - name: Build Package + run: | + hatch build --clean + - name: Publish to PyPI + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + twine upload dist/* diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml deleted file mode 100644 index a98e986..0000000 --- a/.github/workflows/publish-release-docs.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Publish Release Docs - -on: - release: - types: [published] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - 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 - 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 diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index df91de1..6a4de7b 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -17,22 +17,19 @@ 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 -r requirements/build-docs.txt - pip install -r requirements/check-types.txt - pip install -r requirements/check-style.txt + pip3 --quiet install --upgrade hatch uv ruff - name: Check docs build run: | - linkcheckMarkdown docs/ -v -r - linkcheckMarkdown README.md -v -r - linkcheckMarkdown CHANGELOG.md -v -r - cd docs - mkdocs build --strict + hatch run docs:build + hatch run docs:linkcheck - name: Check docs examples run: | - mypy --show-error-codes docs/examples/python/ ruff check docs/examples/python/ diff --git a/.github/workflows/test-src.yaml b/.github/workflows/test-src.yaml deleted file mode 100644 index 687741b..0000000 --- a/.github/workflows/test-src.yaml +++ /dev/null @@ -1,40 +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 - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Latest Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install Python Dependencies - run: pip install -r requirements/test-run.txt - - name: Run Tests - run: nox -t test -- --coverage diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml new file mode 100644 index 0000000..6f1aebf --- /dev/null +++ b/.github/workflows/test-src.yml @@ -0,0 +1,68 @@ +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 + - 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: | + pip3 install hatch uv + - name: Run Tests + run: | + hatch test --cover --python ${{ matrix.python-version }} + mv .coverage ".coverage.py${{ matrix.python-version }}" + - name: Upload coverage data + uses: actions/upload-artifact@v4 + with: + name: "coverage-data-py${{ matrix.python-version }}" + path: ".coverage.py${{ matrix.python-version }}" + if-no-files-found: error + include-hidden-files: true + retention-days: 7 + coverage: + needs: + - source + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Latest Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install Python Dependencies + run: python -m pip install --upgrade coverage[toml] + - name: Download data + uses: actions/download-artifact@v4 + with: + merge-multiple: true + - name: Combine coverage and fail if it's <100% + run: | + python -m coverage combine + python -m coverage html --skip-covered --skip-empty + python -m coverage report --fail-under=100 + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: htmlcov diff --git a/.github/workflows/test-style.yml b/.github/workflows/test-style.yml new file mode 100644 index 0000000..c1e45ab --- /dev/null +++ b/.github/workflows/test-style.yml @@ -0,0 +1,27 @@ +name: Test Style + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test-style: + 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: | + pip3 install hatch uv + - name: Run Tests + run: | + hatch fmt --check diff --git a/CHANGELOG.md b/CHANGELOG.md index a255a52..a59b1e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,10 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +### Changed + +- JavaScript bundle is now created using [`Bun`](https://bun.sh/) +- Python package is now built using [`Hatch`](https://hatch.pypa.io/) ## [1.0.0] - 2024-10-18 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 71f4855..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -recursive-include src/reactpy_router/static * -include src/reactpy_router/py.typed diff --git a/docs/examples/python/basic-routing.py b/docs/examples/python/basic_routing.py similarity index 100% rename from docs/examples/python/basic-routing.py rename to docs/examples/python/basic_routing.py diff --git a/docs/examples/python/basic-routing-more-routes.py b/docs/examples/python/basic_routing_more_routes.py similarity index 100% rename from docs/examples/python/basic-routing-more-routes.py rename to docs/examples/python/basic_routing_more_routes.py diff --git a/docs/examples/python/nested-routes.py b/docs/examples/python/nested_routes.py similarity index 89% rename from docs/examples/python/nested-routes.py rename to docs/examples/python/nested_routes.py index 01ffb18..e146d90 100644 --- a/docs/examples/python/nested-routes.py +++ b/docs/examples/python/nested_routes.py @@ -1,3 +1,4 @@ +import operator from typing import TypedDict from reactpy import component, html, run @@ -41,7 +42,7 @@ def home(): @component def all_messages(): - last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])} + last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=operator.itemgetter("id"))} messages = [] for msg in last_messages.values(): @@ -63,15 +64,13 @@ def messages_with(*names): messages = [msg for msg in message_data if tuple(msg["with"]) == names] return html.div( html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - f"{msg['from'] or 'You'}: {msg['message']}", - ) - for msg in messages - ] - ), + html.ul([ + html.li( + {"key": msg["id"]}, + f"{msg['from'] or 'You'}: {msg['message']}", + ) + for msg in messages + ]), ) diff --git a/docs/examples/python/route-links.py b/docs/examples/python/route_links.py similarity index 100% rename from docs/examples/python/route-links.py rename to docs/examples/python/route_links.py diff --git a/docs/examples/python/route-parameters.py b/docs/examples/python/route_parameters.py similarity index 88% rename from docs/examples/python/route-parameters.py rename to docs/examples/python/route_parameters.py index a794742..6953984 100644 --- a/docs/examples/python/route-parameters.py +++ b/docs/examples/python/route_parameters.py @@ -1,3 +1,4 @@ +import operator from typing import TypedDict from reactpy import component, html, run @@ -39,7 +40,7 @@ def home(): @component def all_messages(): - last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])} + last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=operator.itemgetter("id"))} messages = [] for msg in last_messages.values(): msg_hyperlink = link( @@ -61,15 +62,13 @@ def messages_with(): messages = [msg for msg in message_data if tuple(msg["with"]) == names] return html.div( html.h1(f"Messages with {', '.join(names)} đŸ’Ŧ"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - f"{msg['from'] or 'You'}: {msg['message']}", - ) - for msg in messages - ] - ), + html.ul([ + html.li( + {"key": msg["id"]}, + f"{msg['from'] or 'You'}: {msg['message']}", + ) + for msg in messages + ]), ) diff --git a/docs/examples/python/use-params.py b/docs/examples/python/use_params.py similarity index 100% rename from docs/examples/python/use-params.py rename to docs/examples/python/use_params.py diff --git a/docs/examples/python/use-search-params.py b/docs/examples/python/use_search_params.py similarity index 100% rename from docs/examples/python/use-search-params.py rename to docs/examples/python/use_search_params.py diff --git a/docs/src/index.md b/docs/src/index.md index 969d43c..0fd0196 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -13,5 +13,5 @@ You're now ready to start building your own ReactPy applications with URL routin === "components.py" ```python - {% include "../examples/python/basic-routing.py" %} + {% include "../examples/python/basic_routing.py" %} ``` diff --git a/docs/src/learn/hooks.md b/docs/src/learn/hooks.md index 7ed3821..38e8e66 100644 --- a/docs/src/learn/hooks.md +++ b/docs/src/learn/hooks.md @@ -13,7 +13,7 @@ The [`use_search_params`][reactpy_router.use_search_params] hook can be used to === "components.py" ```python - {% include "../../examples/python/use-search-params.py" %} + {% include "../../examples/python/use_search_params.py" %} ``` ## Use Parameters @@ -23,5 +23,5 @@ The [`use_params`][reactpy_router.use_params] hook can be used to access route p === "components.py" ```python - {% include "../../examples/python/use-params.py" %} + {% include "../../examples/python/use_params.py" %} ``` diff --git a/docs/src/learn/routers-routes-and-links.md b/docs/src/learn/routers-routes-and-links.md index f0abfc1..f185514 100644 --- a/docs/src/learn/routers-routes-and-links.md +++ b/docs/src/learn/routers-routes-and-links.md @@ -16,7 +16,7 @@ Here's a basic example showing how to use `#!python browser_router` with two rou === "components.py" ```python - {% include "../../examples/python/basic-routing.py" %} + {% include "../../examples/python/basic_routing.py" %} ``` Here we'll note some special syntax in the route path for the second route. The `#!python "any"` type is a wildcard that will match any path. This is useful for creating a default page or error page such as "404 NOT FOUND". @@ -65,5 +65,5 @@ Links between routes should be created using the [link][reactpy_router.link] com === "components.py" ```python - {% include "../../examples/python/route-links.py" %} + {% include "../../examples/python/route_links.py" %} ``` diff --git a/docs/src/learn/your-first-app.md b/docs/src/learn/your-first-app.md index 8ffb3d7..398d612 100644 --- a/docs/src/learn/your-first-app.md +++ b/docs/src/learn/your-first-app.md @@ -36,7 +36,7 @@ The first step is to create a basic router that will display the home page when === "components.py" ```python - {% include "../../examples/python/basic-routing.py" %} + {% include "../../examples/python/basic_routing.py" %} ``` When navigating to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) you should see `Home Page 🏠`. However, if you go to any other route you will instead see `Missing Link 🔗‍đŸ’Ĩ`. @@ -46,7 +46,7 @@ With this foundation you can start adding more routes. === "components.py" ```python - {% include "../../examples/python/basic-routing-more-routes.py" %} + {% include "../../examples/python/basic_routing_more_routes.py" %} ``` With this change you can now also go to [`/messages`](http://127.0.0.1:8000/messages) to see `Messages đŸ’Ŧ`. @@ -58,7 +58,7 @@ Instead of using the standard `#!python reactpy.html.a` element to create links === "components.py" ```python - {% include "../../examples/python/route-links.py" %} + {% include "../../examples/python/route_links.py" %} ``` Now, when you go to the home page, you can click `Messages` link to go to [`/messages`](http://127.0.0.1:8000/messages). @@ -70,7 +70,7 @@ Routes can be nested in order to construct more complicated application structur === "components.py" ```python - {% include "../../examples/python/nested-routes.py" %} + {% include "../../examples/python/nested_routes.py" %} ``` ## Adding Route Parameters @@ -84,5 +84,5 @@ If we take this information and apply it to our growing example application we'd === "components.py" ```python - {% include "../../examples/python/route-parameters.py" %} + {% include "../../examples/python/route_parameters.py" %} ``` diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 670ac44..0000000 --- a/noxfile.py +++ /dev/null @@ -1,52 +0,0 @@ -from pathlib import Path - -from nox import Session, session - -ROOT_DIR = Path(__file__).parent - - -@session(tags=["test"]) -def test_python(session: Session) -> None: - install_requirements_file(session, "test-env") - session.install(".[all]") - session.run("playwright", "install", "chromium") - - posargs = session.posargs[:] - - if "--coverage" in posargs: - posargs += ["--cov=reactpy_router", "--cov-report=term"] - posargs.remove("--coverage") - session.install("-e", ".") - else: - session.log("Coverage won't be checked unless `-- --coverage` is defined.") - - session.run("pytest", "tests", *posargs) - - -@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_router", "tests") - - -@session(tags=["test"]) -def test_style(session: Session) -> None: - 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") - install_requirements_file(session, "build-pkg") - 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 09826fd..5ffcba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,129 @@ [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] -ignore_missing_imports = true -warn_unused_configs = true -warn_redundant_casts = true -warn_unused_ignores = true -check_untyped_defs = true +[project] +name = "reactpy_router" +description = "A URL router for ReactPy." +readme = "README.md" +keywords = ["React", "ReactJS", "ReactPy", "components"] +license = "MIT" +authors = [{ name = "Mark Bakhit", email = "archiethemonger@gmail.com" }] +requires-python = ">=3.9" +classifiers = [ + "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 = ["reactpy>=1.0.0", "typing_extensions"] +dynamic = ["version"] +urls.Changelog = "https://reactive-python.github.io/reactpy-router/latest/about/changelog/" +urls.Documentation = "https://reactive-python.github.io/reactpy-router/latest/" +urls.Source = "https://github.com/reactive-python/reactpy-router" + +[tool.hatch.version] +path = "src/reactpy_router/__init__.py" + +[tool.hatch.build.targets.sdist] +include = ["/src"] +artifacts = ["/src/reactpy_router/static/bundle.js"] + +[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.js --outfile src/reactpy_router/static/bundle.js --minify", +] +artifacts = [] + +# >>> Hatch Tests <<< + +[tool.hatch.envs.hatch-test] +extra-dependencies = ["pytest-sugar", "anyio", "reactpy[testing,starlette]"] +randomize = true +matrix-name-format = "{variable}-{value}" + +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.9", "3.10", "3.11", "3.12"] + +# >>> Hatch Documentation Scripts <<< + +[tool.hatch.envs.docs] +template = "docs" +detached = true +dependencies = [ + "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", + "mike", + "mkdocstrings[python]", + "black", + "reactpy_router @ {root:uri}", +] + +[tool.hatch.envs.docs.scripts] +serve = ["cd docs && mkdocs serve"] +build = ["cd docs && mkdocs build --strict"] +linkcheck = [ + "linkcheckMarkdown docs/ -v -r", + "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"] + + +# >>> Generic Tools <<< [tool.ruff] -lint.ignore = ["E501"] -lint.isort.known-first-party = ["src", "tests"] -extend-exclude = [".venv/*", ".eggs/*", ".nox/*", "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 +] +lint.preview = true +extend-exclude = [".venv/*", ".eggs/*", "build/*"] [tool.pytest.ini_options] -testpaths = "tests" +addopts = """\ + --strict-config + --strict-markers + """ + +[tool.coverage.run] +branch = true +parallel = true +source = ["src/", "tests/"] + +[tool.coverage.paths] +source = ["src/"] + +[tool.coverage.report] +show_missing = true +exclude_lines = ["pragma: no cover", "...", "raise NotImplementedError"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7c5eed9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ --r requirements/build-docs.txt --r requirements/build-pkg.txt --r requirements/check-style.txt --r requirements/check-types.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 57805cb..0000000 --- a/requirements/build-docs.txt +++ /dev/null @@ -1,13 +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 -mkdocstrings[python] -black # for mkdocstrings automatic code formatting -. diff --git a/requirements/build-pkg.txt b/requirements/build-pkg.txt deleted file mode 100644 index cd3dc5a..0000000 --- a/requirements/build-pkg.txt +++ /dev/null @@ -1,4 +0,0 @@ -twine -wheel -setuptools -nodejs-bin==18.4.0a4 diff --git a/requirements/check-style.txt b/requirements/check-style.txt deleted file mode 100644 index af3ee57..0000000 --- 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 d4d2f1b..0000000 --- a/requirements/check-types.txt +++ /dev/null @@ -1,2 +0,0 @@ -mypy -reactpy diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt deleted file mode 100644 index 9f673a3..0000000 --- a/requirements/pkg-deps.txt +++ /dev/null @@ -1,2 +0,0 @@ -reactpy >=1 -typing_extensions diff --git a/requirements/test-env.txt b/requirements/test-env.txt deleted file mode 100644 index 0681f8b..0000000 --- a/requirements/test-env.txt +++ /dev/null @@ -1,5 +0,0 @@ -twine -pytest -anyio -pytest-cov -reactpy[testing,starlette] diff --git a/requirements/test-run.txt b/requirements/test-run.txt deleted file mode 100644 index 816817c..0000000 --- a/requirements/test-run.txt +++ /dev/null @@ -1 +0,0 @@ -nox diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 826acaa..0000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[coverage:report] -fail_under = 100 -show_missing = True -skip_covered = True -sort = Miss -exclude_lines = - pragma: no cover - \.\.\. - raise NotImplementedError diff --git a/setup.py b/setup.py deleted file mode 100644 index 2bf91bc..0000000 --- a/setup.py +++ /dev/null @@ -1,131 +0,0 @@ -from __future__ import print_function - -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_router" -root_dir = Path(__file__).parent -src_dir = root_dir / "src" -package_dir = src_dir / name - - -# ----------------------------------------------------------------------------- -# General Package Info -# ----------------------------------------------------------------------------- -package = { - "name": name, - "python_requires": ">=3.9", - "packages": find_namespace_packages(src_dir), - "package_dir": {"": "src"}, - "description": "A URL router for ReactPy.", - "author": "Ryan Morshead", - "author_email": "ryan.morshead@gmail.com", - "url": "https://github.com/reactive-python/reactpy-router", - "platforms": "Linux, Mac OS X, Windows", - "keywords": ["reactpy", "components"], - "include_package_data": True, - "zip_safe": False, - "classifiers": [ - "Environment :: Web Environment", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development :: User Interfaces", - "Topic :: Software Development :: Widget Sets", - "Typing :: Typed", - ], -} - - -# ----------------------------------------------------------------------------- -# 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): - js_dir = str(src_dir / "js") - - log.info("Installing Javascript...") - result = npm.call(["install"], cwd=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=js_dir) - if result != 0: - log.error(traceback.format_exc()) - log.error("Failed to build Javascript") - raise RuntimeError("Failed to build Javascript") - - 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/js/.eslintrc.json b/src/js/.eslintrc.json deleted file mode 100644 index 57e5f54..0000000 --- a/src/js/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "env": { - "browser": true, - "node": true, - "es2021": true - }, - "extends": ["eslint:recommended", "plugin:react/recommended"], - "overrides": [], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["react"], - "rules": { - "react/prop-types": "off" - }, - "settings": { - "react": { - "version": "detect" - } - } -} diff --git a/src/js/bun.lockb b/src/js/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..ca1a00b587473dd48b0aaa96be3ef1ba9788feaf GIT binary patch literal 106219 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≤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 F{6FhQho=Al literal 0 HcmV?d00001 diff --git a/src/js/eslint.config.mjs b/src/js/eslint.config.mjs new file mode 100644 index 0000000..320e9f8 --- /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 b1a7859..0000000 --- a/src/js/package-lock.json +++ /dev/null @@ -1,4553 +0,0 @@ -{ - "name": "reactpy-router", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "reactpy-router", - "dependencies": { - "@rollup/plugin-typescript": "^12.1.1", - "preact": "^10.24.3", - "tslib": "^2.8.0" - }, - "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-replace": "^6.0.1", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "prettier": "^3.3.3", - "rollup": "^4.24.0", - "typescript": "^5.6.3" - } - }, - "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.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "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/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "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": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "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/@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", - "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "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": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", - "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", - "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": "12.1.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", - "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", - "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.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", - "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", - "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/pluginutils/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/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.3.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", - "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "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/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "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.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "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.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.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/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/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "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/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-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "dev": true, - "dependencies": { - "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.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "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.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - } - }, - "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.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "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.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "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", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "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.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "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.0", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "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/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.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.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", - "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.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", - "dev": true, - "dependencies": { - "acorn": "^8.8.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/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.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "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.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "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/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.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "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.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "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/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "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": "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/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.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "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/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "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.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "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.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "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-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-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.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "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-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-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-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.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "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/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/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "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==", - "dev": true - }, - "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-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.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", - "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.5", - "object.assign": "^4.1.3" - }, - "engines": { - "node": ">=4.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "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/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": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "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.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "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.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "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.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "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": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "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.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "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/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "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-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/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - }, - "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/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "devOptional": true, - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", - "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-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/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/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "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/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" - }, - "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-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-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.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "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-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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/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" - } - } - }, - "dependencies": { - "@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, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "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" - } - }, - "@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@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 - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true - }, - "@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, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@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 - }, - "@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, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - } - }, - "@rollup/plugin-node-resolve": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", - "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - } - }, - "@rollup/plugin-replace": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", - "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - } - }, - "@rollup/plugin-typescript": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", - "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", - "requires": { - "@rollup/pluginutils": "^5.1.0", - "resolve": "^1.22.1" - } - }, - "@rollup/pluginutils": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", - "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "dependencies": { - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - } - } - }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "dev": true, - "optional": true - }, - "@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true - }, - "@types/react": { - "version": "18.3.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", - "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@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 - }, - "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true - }, - "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, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "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 - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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 - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - } - }, - "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "is-string": "^1.0.7" - } - }, - "array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - } - }, - "array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" - } - }, - "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 - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "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 - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "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, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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 - }, - "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 - }, - "define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", - "dev": true, - "requires": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "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.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" - } - }, - "es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - } - }, - "es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "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, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "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 - }, - "eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "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.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "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", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "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.0", - "string.prototype.matchall": "^4.0.8" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - } - } - }, - "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", - "dev": true - }, - "espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "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, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "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 - }, - "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 - }, - "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 - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "requires": {} - }, - "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, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "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, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "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, - "requires": { - "is-callable": "^1.1.3" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "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 - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "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" - } - }, - "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, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "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 - }, - "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 - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "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 - }, - "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 - }, - "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, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "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, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "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 - }, - "is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "requires": { - "hasown": "^2.0.2" - } - }, - "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, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "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, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "is-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 - }, - "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, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "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 - }, - "is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "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, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "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, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "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, - "requires": { - "call-bind": "^1.0.2" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "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, - "requires": { - "argparse": "^2.0.1" - } - }, - "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 - }, - "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 - }, - "jsx-ast-utils": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", - "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", - "dev": true, - "requires": { - "array-includes": "^3.1.5", - "object.assign": "^4.1.3" - } - }, - "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, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "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, - "requires": { - "p-locate": "^5.0.0" - } - }, - "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 - }, - "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==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "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 - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true - }, - "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 - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "requires": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "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, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "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, - "requires": { - "p-limit": "^3.0.2" - } - }, - "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, - "requires": { - "callsites": "^3.0.0" - } - }, - "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 - }, - "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 - }, - "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 - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true - }, - "preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==" - }, - "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 - }, - "prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true - }, - "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, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true - }, - "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 - }, - "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 - }, - "regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - } - }, - "resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "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 - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "devOptional": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", - "@types/estree": "1.0.6", - "fsevents": "~2.3.2" - } - }, - "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, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - }, - "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, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "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 - }, - "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, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" - } - }, - "string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "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 - }, - "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, - "requires": { - "has-flag": "^4.0.0" - } - }, - "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==" - }, - "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 - }, - "tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" - }, - "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, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "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 - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - } - }, - "typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==" - }, - "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, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "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, - "requires": { - "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" - } - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true - }, - "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 - }, - "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 - } - } -} diff --git a/src/js/package.json b/src/js/package.json index b9370a4..313fab9 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,32 +1,16 @@ { - "name": "reactpy-router", - "description": "A URL router for ReactPy", - "author": "Mark Bakhit", - "repository": { - "type": "git", - "url": "https://github.com/reactive-python/reactpy-router" - }, - "main": "src/index.tsx", "scripts": { - "build": "rollup --config", "format": "prettier --write . && eslint --fix", "check": "prettier --check . && eslint" }, "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-replace": "^6.0.1", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "prettier": "^3.3.3", - "rollup": "^4.24.0", - "typescript": "^5.6.3" + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.1", + "prettier": "^3.3.3" }, "dependencies": { - "@rollup/plugin-typescript": "^12.1.1", - "preact": "^10.24.3", - "tslib": "^2.8.0" + "preact": "^10.24.3" } } diff --git a/src/js/rollup.config.mjs b/src/js/rollup.config.mjs deleted file mode 100644 index 0410a35..0000000 --- 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.ts", - output: { - file: "../reactpy_router/static/bundle.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/tsconfig.json b/src/js/tsconfig.json deleted file mode 100644 index 2060912..0000000 --- a/src/js/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "esnext", - "moduleResolution": "node", - "jsx": "react", - "allowSyntheticDefaultImports": true - } -} diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index cd07f57..1e5ea46 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -2,16 +2,16 @@ __version__ = "1.0.0" -from .components import link, navigate, route -from .hooks import use_params, use_search_params -from .routers import browser_router, create_router +from reactpy_router.components import link, navigate, route +from reactpy_router.hooks import use_params, use_search_params +from reactpy_router.routers import browser_router, create_router __all__ = ( + "browser_router", "create_router", "link", + "navigate", "route", - "browser_router", "use_params", "use_search_params", - "navigate", ) diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py index 657c558..6a84799 100644 --- a/src/reactpy_router/components.py +++ b/src/reactpy_router/components.py @@ -1,19 +1,21 @@ from __future__ import annotations from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import urljoin from uuid import uuid4 from reactpy import component, html, use_connection from reactpy.backend.types import Location -from reactpy.core.component import Component -from reactpy.core.types import VdomDict from reactpy.web.module import export, module_from_file from reactpy_router.hooks import _use_route_state from reactpy_router.types import Route +if TYPE_CHECKING: + from reactpy.core.component import Component + from reactpy.core.types import Key, VdomDict + History = export( module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), ("History"), @@ -40,7 +42,7 @@ link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8") -def link(attributes: dict[str, Any], *children: Any) -> Component: +def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) -> Component: """ Create a link with the given attributes and children. @@ -51,7 +53,7 @@ def link(attributes: dict[str, Any], *children: Any) -> Component: Returns: A link component with the specified attributes and children. """ - return _link(attributes, *children) + return _link(attributes, *children, key=key) @component @@ -68,7 +70,8 @@ def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: if "href" in attributes and "to" not in attributes: attributes["to"] = attributes.pop("href") if "to" not in attributes: # pragma: no cover - raise ValueError("The `to` attribute is required for the `Link` component.") + msg = "The `to` attribute is required for the `Link` component." + raise ValueError(msg) to = attributes.pop("to") attrs = { @@ -132,7 +135,7 @@ def route(path: str, element: Any | None, *routes: Route) -> Route: return Route(path, element, routes) -def navigate(to: str, replace: bool = False) -> Component: +def navigate(to: str, replace: bool = False, key: Key | None = None) -> Component: """ Navigate to a specified URL. @@ -146,7 +149,7 @@ def navigate(to: str, replace: bool = False) -> Component: Returns: The component responsible for navigation. """ - return _navigate(to, replace) + return _navigate(to, replace, key=key) @component diff --git a/src/reactpy_router/hooks.py b/src/reactpy_router/hooks.py index add8953..2480440 100644 --- a/src/reactpy_router/hooks.py +++ b/src/reactpy_router/hooks.py @@ -1,12 +1,15 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import parse_qs from reactpy import create_context, use_context, use_location -from reactpy.types import Context -from reactpy_router.types import RouteState +from reactpy_router.types import RouteState # noqa: TCH001 + +if TYPE_CHECKING: + from reactpy.types import Context + _route_state_context: Context[RouteState | None] = create_context(None) @@ -14,10 +17,11 @@ def _use_route_state() -> RouteState: route_state = use_context(_route_state_context) if route_state is None: # pragma: no cover - raise RuntimeError( + msg = ( "ReactPy-Router was unable to find a route context. Are you " "sure this hook/component is being called within a router?" ) + raise RuntimeError(msg) return route_state diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py index 55c6a01..48de28f 100644 --- a/src/reactpy_router/resolvers.py +++ b/src/reactpy_router/resolvers.py @@ -1,10 +1,12 @@ from __future__ import annotations import re -from typing import Any +from typing import TYPE_CHECKING, Any from reactpy_router.converters import CONVERTERS -from reactpy_router.types import ConversionInfo, ConverterMapping, Route + +if TYPE_CHECKING: + from reactpy_router.types import ConversionInfo, ConverterMapping, Route __all__ = ["StarletteResolver"] @@ -48,7 +50,8 @@ def parse_path(self, path: str) -> re.Pattern[str]: try: conversion_info = self.registered_converters[param_type] except KeyError as e: - raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}") from e + msg = f"Unknown conversion type {param_type!r} in {path!r}" + raise ValueError(msg) from e # Add the string before the match to the pattern pattern += re.escape(path[last_match_end : match.start()]) diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index a815f0d..abdbc5e 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -4,18 +4,23 @@ from dataclasses import replace from logging import getLogger -from typing import Any, Iterator, Literal, Sequence, cast +from typing import TYPE_CHECKING, Any, Literal, cast from reactpy import component, use_memo, use_state from reactpy.backend.hooks import ConnectionContext, use_connection from reactpy.backend.types import Connection, Location -from reactpy.core.component import Component from reactpy.types import ComponentType, VdomDict from reactpy_router.components import FirstLoad, History from reactpy_router.hooks import RouteState, _route_state_context from reactpy_router.resolvers import StarletteResolver -from reactpy_router.types import CompiledRoute, Resolver, Router, RouteType + +if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + + from reactpy.core.component import Component + + from reactpy_router.types import CompiledRoute, Resolver, Router, RouteType __all__ = ["browser_router", "create_router"] _logger = getLogger(__name__) diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index 87f7d7f..81404b7 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -3,14 +3,18 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Callable, Sequence, TypedDict, TypeVar +from typing import TYPE_CHECKING, Any, Callable, TypedDict, TypeVar -from reactpy.backend.types import Location -from reactpy.core.component import Component from reactpy.core.vdom import is_vdom -from reactpy.types import Key from typing_extensions import Protocol, Self, TypeAlias +if TYPE_CHECKING: + from collections.abc import Sequence + + from reactpy.backend.types import Location + from reactpy.core.component import Component + from reactpy.types import Key + ConversionFunc: TypeAlias = Callable[[str], Any] """A function that converts a string to a specific type.""" diff --git a/tests/conftest.py b/tests/conftest.py index 7d6f0ed..3463e16 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import os +import subprocess import pytest from playwright.async_api import async_playwright @@ -16,6 +17,12 @@ def pytest_addoption(parser) -> None: ) +def pytest_sessionstart(session): + """Rebuild the project before running the tests to get the latest JavaScript""" + subprocess.run(["hatch", "build", "--clean"], check=True) + subprocess.run(["playwright", "install", "chromium"], check=True) + + @pytest.fixture(scope="session") async def display(backend, browser): async with DisplayFixture(backend, browser) as display_fixture: diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 4e8a669..a5dad38 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -46,7 +46,7 @@ def test_parse_path(): def test_parse_path_unkown_conversion(): resolver = StarletteResolver(route("/", None)) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unknown conversion type 'unknown' in '/a/{b:unknown}/c'"): resolver.parse_path("/a/{b:unknown}/c") diff --git a/tests/test_router.py b/tests/test_router.py index 1a4d95c..ddef544 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -136,11 +136,12 @@ def sample(): await display.show(sample) - for path, expected_params in [ + for path, _expected_params in [ ("/first/1", {"first": "1"}), ("/first/1/second/2", {"first": "1", "second": "2"}), ("/first/1/second/2/third/3", {"first": "1", "second": "2", "third": "3"}), ]: + expected_params = _expected_params await display.goto(path) await display.page.wait_for_selector("#success") From 198c67545d59d36c30a8bf4eed38386c4caec6b2 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 24 Oct 2024 01:26:25 -0700 Subject: [PATCH 13/22] v1.0.1 (#42) --- .github/workflows/publish-develop-docs.yml | 3 +-- .github/workflows/publish-latest-docs.yml | 3 +-- CHANGELOG.md | 4 ++++ src/reactpy_router/__init__.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index 65a1bbb..584be64 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -16,8 +16,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install -r requirements/build-docs.txt - - name: Install dependecies + - name: Install dependencies run: | pip install --upgrade hatch uv - name: Configure Git diff --git a/.github/workflows/publish-latest-docs.yml b/.github/workflows/publish-latest-docs.yml index 7ce0c00..3ff83a7 100644 --- a/.github/workflows/publish-latest-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -16,8 +16,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install -r requirements/build-docs.txt - - name: Install dependecies + - name: Install dependencies run: | pip install --upgrade hatch uv - name: Configure Git diff --git a/CHANGELOG.md b/CHANGELOG.md index a59b1e1..e0be82e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ Using the following categories, list your changes in this order: ## [Unreleased] +- Nothing (yet)! + +## [1.0.1] - 2024-10-24 + ### Changed - JavaScript bundle is now created using [`Bun`](https://bun.sh/) diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index 1e5ea46..ce138fd 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -1,5 +1,5 @@ # the version is statically loaded by setup.py -__version__ = "1.0.0" +__version__ = "1.0.1" from reactpy_router.components import link, navigate, route From 7f3725329b82aa1b29b624a53187b778078e72ae Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 24 Oct 2024 01:51:42 -0700 Subject: [PATCH 14/22] Fix release workflow and update contrib docs (#43) * Fix release workflow and update contrib docs --- .github/workflows/publish-py.yaml | 1 - CHANGELOG.md | 3 +- docs/mkdocs.yml | 4 +- docs/src/about/code.md | 51 ---------------------- docs/src/about/contributing.md | 70 +++++++++++++++++++++++++++++++ docs/src/about/docs.md | 45 -------------------- docs/src/dictionary.txt | 4 ++ 7 files changed, 77 insertions(+), 101 deletions(-) delete mode 100644 docs/src/about/code.md create mode 100644 docs/src/about/contributing.md delete mode 100644 docs/src/about/docs.md diff --git a/.github/workflows/publish-py.yaml b/.github/workflows/publish-py.yaml index 430a3f0..d69b475 100644 --- a/.github/workflows/publish-py.yaml +++ b/.github/workflows/publish-py.yaml @@ -22,7 +22,6 @@ jobs: - name: Install dependencies run: | pip3 --quiet install --upgrade hatch uv twine - pip install -r requirements/build-pkg.txt - name: Build Package run: | hatch build --clean diff --git a/CHANGELOG.md b/CHANGELOG.md index e0be82e..c0a2658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,7 +98,8 @@ Using the following categories, list your changes in this order: - Rename `configure` to `create_router`. - Rename from `idom-router` to `reactpy-router`. -[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.0...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.1...HEAD +[1.0.1]: https://github.com/reactive-python/reactpy-router/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...1.0.0 [0.1.1]: https://github.com/reactive-python/reactpy-router/compare/0.1.0...0.1.1 [0.1.0]: https://github.com/reactive-python/reactpy-router/compare/0.0.1...0.1.0 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ebf8b0e..8f8a1a1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -14,9 +14,7 @@ nav: - Types: reference/types.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-router/discussions - Discord: https://discord.gg/uNb5P4hA9X diff --git a/docs/src/about/code.md b/docs/src/about/code.md deleted file mode 100644 index 0eda9ee..0000000 --- a/docs/src/about/code.md +++ /dev/null @@ -1,51 +0,0 @@ -## Overview - -

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

- ---- - -## 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-router.git -cd reactpy-router -``` - -Then, by running the command below you can install the dependencies needed to run the ReactPy-Router development environment. - -```bash linenums="0" -pip install -r requirements.txt --upgrade --verbose -``` - -## 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 run: - -```bash linenums="0" -nox -t test -- --headless -``` - -## 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 0000000..d23b77f --- /dev/null +++ b/docs/src/about/contributing.md @@ -0,0 +1,70 @@ +## 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/) + +Once you finish installing these dependencies, you can clone this repository: + +```shell +git clone https://github.com/reactive-python/reactpy-router.git +cd reactpy-router +``` + +## 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 -k test_navigate_with_link` | Run only a specific test | + +??? 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 directly passed on 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 | + +??? 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 | + +### 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 4c1f566..0000000 --- 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-router.git -cd reactpy-router -``` - -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. - -## Creating a pull request - -{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 64ed74d..435700c 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -38,3 +38,7 @@ misconfigurations backhaul sublicense contravariant +formatters +linters +linting +pytest From 7ffa76fa460a51ec24508d6dd068bd954b45d144 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 24 Oct 2024 01:58:42 -0700 Subject: [PATCH 15/22] Fix tests badge in readme (#44) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a868aac..4fcafc9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ReactPy Router

- - + + From 23c0a77191512fe84cb04638d70a928c36c32a8a Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 24 Oct 2024 13:58:53 -0700 Subject: [PATCH 16/22] 1.0.2 (Add bundle.js to wheel) (#45) --- CHANGELOG.md | 13 ++++++++++--- pyproject.toml | 3 +++ src/reactpy_router/__init__.py | 3 +-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a2658..26a01ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,12 +36,18 @@ Using the following categories, list your changes in this order: - Nothing (yet)! +## [1.0.2] - 2024-10-24 + +### Fixed + +- Fix python `wheel` missing `bundle.js` file. + ## [1.0.1] - 2024-10-24 ### Changed -- JavaScript bundle is now created using [`Bun`](https://bun.sh/) -- Python package is now built using [`Hatch`](https://hatch.pypa.io/) +- JavaScript bundle is now created using [`Bun`](https://bun.sh/). +- Python package is now built using [`Hatch`](https://hatch.pypa.io/). ## [1.0.0] - 2024-10-18 @@ -98,7 +104,8 @@ Using the following categories, list your changes in this order: - Rename `configure` to `create_router`. - Rename from `idom-router` to `reactpy-router`. -[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.1...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.2...HEAD +[1.0.2]: https://github.com/reactive-python/reactpy-router/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/reactive-python/reactpy-router/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...1.0.0 [0.1.1]: https://github.com/reactive-python/reactpy-router/compare/0.1.0...0.1.1 diff --git a/pyproject.toml b/pyproject.toml index 5ffcba2..956b2a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ path = "src/reactpy_router/__init__.py" include = ["/src"] artifacts = ["/src/reactpy_router/static/bundle.js"] +[tool.hatch.build.targets.wheel] +artifacts = ["/src/reactpy_router/static/bundle.js"] + [tool.hatch.metadata] license-files = { paths = ["LICENSE.md"] } diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index ce138fd..41a7851 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -1,5 +1,4 @@ -# the version is statically loaded by setup.py -__version__ = "1.0.1" +__version__ = "1.0.2" from reactpy_router.components import link, navigate, route From 785d3d2461646d7173963af498ce3371f9781504 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Tue, 19 Nov 2024 03:23:17 +0000 Subject: [PATCH 17/22] Fix double rendering (#46) Co-authored-by: Archmonger <16909269+Archmonger@users.noreply.github.com> --- CHANGELOG.md | 2 +- src/reactpy_router/routers.py | 20 ++++++++---- tests/test_rendering.py | 60 +++++++++++++++++++++++++++++++++++ tests/utils.py | 7 ++++ 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 tests/test_rendering.py create mode 100644 tests/utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a01ca..f9cc516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +- Fix double rendering issue on initial page load ## [1.0.2] - 2024-10-24 diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index abdbc5e..d8e75f2 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -74,13 +74,19 @@ def router( match = use_memo(lambda: _match_route(resolvers, location, select="first")) if match: - route_elements = [ - _route_state_context( - element, - value=RouteState(set_location, params), - ) - for element, params in match - ] + if first_load: + # We need skip rendering the application on 'first_load' to avoid + # rendering it twice. The second render occurs following + # the impending on_history_change event + route_elements = [] + else: + route_elements = [ + _route_state_context( + element, + value=RouteState(set_location, params), + ) + for element, params in match + ] def on_history_change(event: dict[str, Any]) -> None: """Callback function used within the JavaScript `History` component.""" diff --git a/tests/test_rendering.py b/tests/test_rendering.py new file mode 100644 index 0000000..6a78ecc --- /dev/null +++ b/tests/test_rendering.py @@ -0,0 +1,60 @@ +import pytest +from reactpy import component, html +from reactpy.testing import DisplayFixture + +from reactpy_router import browser_router, route + +from .utils import page_load_complete + + +@pytest.mark.anyio +async def test_router_simple(display: DisplayFixture): + """Confirm the number of rendering operations when new pages are first loaded""" + root_render_count = 0 + home_page_render_count = 0 + not_found_render_count = 0 + + @component + def root(): + nonlocal root_render_count + root_render_count += 1 + + @component + def home_page(): + nonlocal home_page_render_count + home_page_render_count += 1 + return html.h1("Home Page 🏠") + + @component + def not_found(): + nonlocal not_found_render_count + not_found_render_count += 1 + return html.h1("Missing Link 🔗‍đŸ’Ĩ") + + return browser_router( + route("/", home_page()), + route("{404:any}", not_found()), + ) + + await display.show(root) + await page_load_complete(display.page) + + assert root_render_count == 1 + assert home_page_render_count == 1 + assert not_found_render_count == 0 + + await display.goto("/xxx") + await page_load_complete(display.page) + + assert root_render_count == 2 + assert home_page_render_count == 1 + assert not_found_render_count == 1 + + await display.goto("/yyy") + await page_load_complete(display.page) + + assert root_render_count == 3 + assert home_page_render_count == 1 + assert not_found_render_count == 2 + + assert True diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..2b7bc2e --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,7 @@ +from playwright.async_api._generated import Page + + +async def page_load_complete(page: Page) -> None: + """Only return when network is idle and DOM has loaded""" + await page.wait_for_load_state("networkidle") + await page.wait_for_load_state("domcontentloaded") From 28d60a21b657e8aa192bd1edfd082ac5eb841cd3 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Thu, 21 Nov 2024 16:08:54 -0800 Subject: [PATCH 18/22] v1.0.3 (#47) --- CHANGELOG.md | 11 +++++++++-- src/reactpy_router/__init__.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9cc516..66617e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,13 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Fix double rendering issue on initial page load +- Nothing (yet)! + +## [1.0.3] - 2024-11-21 + +### Fixed + +- Fix behavior where the page would be rendered twice on initial load ## [1.0.2] - 2024-10-24 @@ -104,7 +110,8 @@ Using the following categories, list your changes in this order: - Rename `configure` to `create_router`. - Rename from `idom-router` to `reactpy-router`. -[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.2...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/1.0.3...HEAD +[1.0.3]: https://github.com/reactive-python/reactpy-router/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/reactive-python/reactpy-router/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/reactive-python/reactpy-router/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...1.0.0 diff --git a/src/reactpy_router/__init__.py b/src/reactpy_router/__init__.py index 41a7851..7ed7d3b 100644 --- a/src/reactpy_router/__init__.py +++ b/src/reactpy_router/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.0.2" +__version__ = "1.0.3" from reactpy_router.components import link, navigate, route From 53f5488df27cf39d1e760c339ce6c9d95709ecd8 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 2 Dec 2024 04:08:38 -0800 Subject: [PATCH 19/22] Synchronize CI with other reactive-python repos (#48) --- .github/workflows/publish-develop-docs.yml | 8 ++- .github/workflows/publish-latest-docs.yml | 7 ++- .github/workflows/publish-py.yaml | 33 ------------ .github/workflows/publish-python.yaml | 27 ++++++++++ .github/workflows/test-docs.yml | 12 ++--- .../{test-style.yml => test-javascript.yml} | 12 ++--- .../{test-src.yml => test-python.yml} | 30 ++++++++--- CHANGELOG.md | 4 +- docs/mkdocs.yml | 2 +- docs/src/about/contributing.md | 4 ++ docs/src/dictionary.txt | 1 + pyproject.toml | 52 +++++++++++++------ 12 files changed, 109 insertions(+), 83 deletions(-) delete mode 100644 .github/workflows/publish-py.yaml create mode 100644 .github/workflows/publish-python.yaml rename .github/workflows/{test-style.yml => test-javascript.yml} (70%) rename .github/workflows/{test-src.yml => test-python.yml} (73%) diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index 584be64..a1434ba 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -4,7 +4,7 @@ on: branches: - main jobs: - deploy: + publish-develop-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,14 +17,12 @@ jobs: with: python-version: 3.x - name: Install dependencies - run: | - pip install --upgrade hatch uv + run: pip install --upgrade pip hatch uv - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - name: Publish Develop Docs - run: | - hatch run docs:deploy_develop + run: hatch run docs:deploy_develop concurrency: group: publish-docs diff --git a/.github/workflows/publish-latest-docs.yml b/.github/workflows/publish-latest-docs.yml index 3ff83a7..0a1e996 100644 --- a/.github/workflows/publish-latest-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -18,13 +18,12 @@ jobs: python-version: 3.x - name: Install dependencies run: | - pip install --upgrade hatch uv + pip install --upgrade pip hatch uv - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - - name: Publish Develop Docs - run: | - hatch run docs:deploy_latest ${{ github.ref_name }} + - 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.yaml b/.github/workflows/publish-py.yaml deleted file mode 100644 index d69b475..0000000 --- a/.github/workflows/publish-py.yaml +++ /dev/null @@ -1,33 +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: - publish-package: - 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: | - pip3 --quiet install --upgrade hatch uv twine - - name: Build Package - run: | - hatch build --clean - - name: Publish to PyPI - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - twine upload dist/* diff --git a/.github/workflows/publish-python.yaml b/.github/workflows/publish-python.yaml new file mode 100644 index 0000000..a2228a7 --- /dev/null +++ b/.github/workflows/publish-python.yaml @@ -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 --upgrade pip hatch uv + - 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 6a4de7b..0a81d18 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -24,12 +24,10 @@ jobs: with: python-version: 3.x - name: Install Python Dependencies - run: | - pip3 --quiet install --upgrade hatch uv ruff + run: pip install --upgrade pip hatch uv + - name: Check documentation links + run: hatch run docs:linkcheck - name: Check docs build - run: | - hatch run docs:build - hatch run docs:linkcheck + run: hatch run docs:build - name: Check docs examples - run: | - ruff check docs/examples/python/ + run: hatch fmt docs --check diff --git a/.github/workflows/test-style.yml b/.github/workflows/test-javascript.yml similarity index 70% rename from .github/workflows/test-style.yml rename to .github/workflows/test-javascript.yml index c1e45ab..5f62c0e 100644 --- a/.github/workflows/test-style.yml +++ b/.github/workflows/test-javascript.yml @@ -1,4 +1,4 @@ -name: Test Style +name: Test on: push: @@ -9,7 +9,7 @@ on: - main jobs: - test-style: + javascript: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -18,10 +18,8 @@ jobs: bun-version: latest - uses: actions/setup-python@v5 with: - python-version: "3.x" + python-version: 3.x - name: Install Python Dependencies - run: | - pip3 install hatch uv + run: pip install --upgrade pip hatch uv - name: Run Tests - run: | - hatch fmt --check + run: hatch run javascript:check diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-python.yml similarity index 73% rename from .github/workflows/test-src.yml rename to .github/workflows/test-python.yml index 6f1aebf..9390316 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-python.yml @@ -11,7 +11,7 @@ on: - cron: "0 0 * * *" jobs: - source: + python-source: runs-on: ubuntu-latest strategy: matrix: @@ -26,8 +26,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install Python Dependencies - run: | - pip3 install hatch uv + run: pip install --upgrade pip hatch uv - name: Run Tests run: | hatch test --cover --python ${{ matrix.python-version }} @@ -40,18 +39,18 @@ jobs: if-no-files-found: error include-hidden-files: true retention-days: 7 - coverage: + + python-coverage: needs: - - source + - python-source runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Use Latest Python - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install Python Dependencies - run: python -m pip install --upgrade coverage[toml] + run: pip install --upgrade coverage[toml] - name: Download data uses: actions/download-artifact@v4 with: @@ -66,3 +65,18 @@ jobs: with: name: coverage-report path: htmlcov + + 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 --upgrade pip hatch uv + - name: Check Python formatting + run: hatch fmt src tests --check diff --git a/CHANGELOG.md b/CHANGELOG.md index 66617e3..e3fa727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,9 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +### Changed + +- Set upper limit on ReactPy version to `<2.0.0`. ## [1.0.3] - 2024-11-21 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 8f8a1a1..28df470 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -145,6 +145,6 @@ site_description: It's React-Router, but in Python. copyright: '©

Reactive Python and affiliates.' repo_url: https://github.com/reactive-python/reactpy-router site_url: https://reactive-python.github.io/reactpy-router -repo_name: ReactPy Router (GitHub) +repo_name: ReactPy Router edit_uri: edit/main/docs/src/ docs_dir: src diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md index d23b77f..c7cf012 100644 --- a/docs/src/about/contributing.md +++ b/docs/src/about/contributing.md @@ -5,6 +5,7 @@ If you plan to make code changes to this repository, you will need to install th - [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: @@ -40,6 +41,8 @@ By utilizing `hatch`, the following commands are available to manage the develop | `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 | ??? tip "Configure your IDE for linting" @@ -54,6 +57,7 @@ By utilizing `hatch`, the following commands are available to manage the develop | `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 diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 435700c..b487e39 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -39,6 +39,7 @@ backhaul sublicense contravariant formatters +linter linters linting pytest diff --git a/pyproject.toml b/pyproject.toml index 956b2a2..6472bdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,10 @@ build-backend = "hatchling.build" requires = ["hatchling", "hatch-build-scripts"] +############################## +# >>> Hatch Build Config <<< # +############################## + [project] name = "reactpy_router" description = "A URL router for ReactPy." @@ -24,7 +28,7 @@ classifiers = [ "Environment :: Web Environment", "Typing :: Typed", ] -dependencies = ["reactpy>=1.0.0", "typing_extensions"] +dependencies = ["reactpy>=1.0.0, <2.0.0", "typing_extensions"] dynamic = ["version"] urls.Changelog = "https://reactive-python.github.io/reactpy-router/latest/about/changelog/" urls.Documentation = "https://reactive-python.github.io/reactpy-router/latest/" @@ -35,10 +39,10 @@ path = "src/reactpy_router/__init__.py" [tool.hatch.build.targets.sdist] include = ["/src"] -artifacts = ["/src/reactpy_router/static/bundle.js"] +artifacts = ["/src/reactpy_router/static/"] [tool.hatch.build.targets.wheel] -artifacts = ["/src/reactpy_router/static/bundle.js"] +artifacts = ["/src/reactpy_router/static/"] [tool.hatch.metadata] license-files = { paths = ["LICENSE.md"] } @@ -53,7 +57,9 @@ commands = [ ] artifacts = [] -# >>> Hatch Tests <<< +############################# +# >>> Hatch Test Runner <<< # +############################# [tool.hatch.envs.hatch-test] extra-dependencies = ["pytest-sugar", "anyio", "reactpy[testing,starlette]"] @@ -63,24 +69,30 @@ matrix-name-format = "{variable}-{value}" [[tool.hatch.envs.hatch-test.matrix]] python = ["3.9", "3.10", "3.11", "3.12"] -# >>> Hatch Documentation Scripts <<< +[tool.pytest.ini_options] +addopts = """\ + --strict-config + --strict-markers + """ + +####################################### +# >>> Hatch Documentation Scripts <<< # +####################################### [tool.hatch.envs.docs] template = "docs" -detached = true dependencies = [ "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", "mike", "mkdocstrings[python]", - "black", - "reactpy_router @ {root:uri}", + "black", # Used by mkdocstrings for auto formatting + "linkcheckmd", ] [tool.hatch.envs.docs.scripts] @@ -94,10 +106,23 @@ linkcheck = [ deploy_latest = ["cd docs && mike deploy --push --update-aliases {args} latest"] deploy_develop = ["cd docs && mike deploy --push develop"] +############################ +# >>> Hatch JS Scripts <<< # +############################ -# >>> Generic Tools <<< +[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 = [".venv/*", ".eggs/*", "build/*"] line-length = 120 format.preview = true lint.extend-ignore = [ @@ -111,13 +136,6 @@ lint.extend-ignore = [ "SLF001", # Private member accessed ] lint.preview = true -extend-exclude = [".venv/*", ".eggs/*", "build/*"] - -[tool.pytest.ini_options] -addopts = """\ - --strict-config - --strict-markers - """ [tool.coverage.run] branch = true From 44f76d231da0403705365e1741342f3dae757835 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Tue, 7 Jan 2025 01:52:33 +0000 Subject: [PATCH 20/22] Fix `link` reference to `currentTarget` (#49) Co-authored-by: Archmonger <16909269+Archmonger@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ src/reactpy_router/static/link.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fa727..6658cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ Using the following categories, list your changes in this order: - Set upper limit on ReactPy version to `<2.0.0`. +### Fixed + +- Fixed bug where `link` element sometimes would sometimes not retrieve the correct `href` attribute. + ## [1.0.3] - 2024-11-21 ### Fixed diff --git a/src/reactpy_router/static/link.js b/src/reactpy_router/static/link.js index 9f78cc5..7ab069b 100644 --- a/src/reactpy_router/static/link.js +++ b/src/reactpy_router/static/link.js @@ -4,7 +4,7 @@ document.querySelector(".UUID").addEventListener( // Prevent default if ctrl isn't pressed if (!event.ctrlKey) { event.preventDefault(); - let to = event.target.getAttribute("href"); + let to = event.currentTarget.getAttribute("href"); let new_url = new URL(to, window.location); // Deduplication needed due to ReactPy rendering bug From 2d79831a5b0ad333b6a66b05bfb62cb098c3f237 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sat, 11 Jan 2025 22:40:16 -0800 Subject: [PATCH 21/22] Refactoring related to ReactPy v1.1.0 (#50) --- .github/workflows/test-python.yml | 15 ++++ CHANGELOG.md | 26 ++---- README.md | 4 +- docs/src/about/contributing.md | 1 + pyproject.toml | 14 +++- src/js/src/components.ts | 101 +++++++++++++++++++++++ src/js/src/index.ts | 130 +----------------------------- src/js/src/types.ts | 4 - src/js/src/utils.ts | 12 ++- src/reactpy_router/components.py | 53 ++---------- src/reactpy_router/routers.py | 31 +++---- src/reactpy_router/static/link.js | 17 ---- src/reactpy_router/types.py | 6 +- 13 files changed, 169 insertions(+), 245 deletions(-) create mode 100644 src/js/src/components.ts delete mode 100644 src/reactpy_router/static/link.js diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 9390316..bbac572 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -80,3 +80,18 @@ jobs: run: pip install --upgrade pip hatch uv - 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 --upgrade pip hatch uv + - name: Run Python type checker + run: hatch run python:type_check diff --git a/CHANGELOG.md b/CHANGELOG.md index 6658cea..1fced87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,25 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +Don't forget to remove deprecated code on each major release! +--> @@ -36,7 +21,10 @@ Using the following categories, list your changes in this order: ### Changed -- Set upper limit on ReactPy version to `<2.0.0`. +- Set maximum ReactPy version to `<2.0.0`. +- Set minimum ReactPy version to `1.1.0`. +- `link` element now calculates URL changes using the client. +- Refactoring related to `reactpy>=1.1.0` changes. ### Fixed diff --git a/README.md b/README.md index 4fcafc9..24ad465 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ReactPy Router

- - + + diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md index c7cf012..82b82f1 100644 --- a/docs/src/about/contributing.md +++ b/docs/src/about/contributing.md @@ -43,6 +43,7 @@ By utilizing `hatch`, the following commands are available to manage the develop | `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" diff --git a/pyproject.toml b/pyproject.toml index 6472bdf..0f3d7f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ "Environment :: Web Environment", "Typing :: Typed", ] -dependencies = ["reactpy>=1.0.0, <2.0.0", "typing_extensions"] +dependencies = ["reactpy>=1.1.0, <2.0.0", "typing_extensions"] dynamic = ["version"] urls.Changelog = "https://reactive-python.github.io/reactpy-router/latest/about/changelog/" urls.Documentation = "https://reactive-python.github.io/reactpy-router/latest/" @@ -53,7 +53,7 @@ installer = "uv" [[tool.hatch.build.hooks.build-scripts.scripts]] commands = [ "bun install --cwd src/js", - "bun build src/js/src/index.js --outfile src/reactpy_router/static/bundle.js --minify", + "bun build src/js/src/index.ts --outfile src/reactpy_router/static/bundle.js --minify", ] artifacts = [] @@ -106,6 +106,16 @@ linkcheck = [ 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 = ["pyright"] + +[tool.hatch.envs.python.scripts] +type_check = ["pyright src"] + ############################ # >>> Hatch JS Scripts <<< # ############################ diff --git a/src/js/src/components.ts b/src/js/src/components.ts new file mode 100644 index 0000000..4712637 --- /dev/null +++ b/src/js/src/components.ts @@ -0,0 +1,101 @@ +import React from "preact/compat"; +import ReactDOM from "preact/compat"; +import { createLocationObject, pushState, replaceState } from "./utils"; +import { HistoryProps, LinkProps, NavigateProps } from "./types"; + +/** + * 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), + }; +} + +/** + * History component that captures browser "history go back" actions and notifies the server. + */ +export function History({ onHistoryChangeCallback }: HistoryProps): null { + // Tell the server about history "popstate" events + React.useEffect(() => { + const listener = () => { + onHistoryChangeCallback(createLocationObject()); + }; + + // Register the event listener + window.addEventListener("popstate", listener); + + // Delete the event listener when the component is unmounted + return () => window.removeEventListener("popstate", listener); + }); + + // Tell the server about the URL during the initial page load + React.useEffect(() => { + onHistoryChangeCallback(createLocationObject()); + return () => {}; + }, []); + return null; +} + +/** + * Link component that captures clicks on anchor links and notifies the server. + * + * This component is not the actual `` link element. It is just an event + * listener for ReactPy-Router's server-side link component. + */ +export function Link({ onClickCallback, linkClass }: LinkProps): null { + React.useEffect(() => { + // Event function that will tell the server about clicks + const handleClick = (event: Event) => { + let click_event = event as MouseEvent; + if (!click_event.ctrlKey) { + event.preventDefault(); + let to = (event.currentTarget as HTMLElement).getAttribute("href"); + pushState(to); + onClickCallback(createLocationObject()); + } + }; + + // Register the event listener + let link = document.querySelector(`.${linkClass}`); + if (link) { + link.addEventListener("click", handleClick); + } else { + console.warn(`Link component with class name ${linkClass} not found.`); + } + + // Delete the event listener when the component is unmounted + return () => { + if (link) { + link.removeEventListener("click", handleClick); + } + }; + }); + return null; +} + +/** + * Client-side portion of the navigate component, that allows the server to command the client to change URLs. + */ +export function Navigate({ + onNavigateCallback, + to, + replace = false, +}: NavigateProps): null { + React.useEffect(() => { + if (replace) { + replaceState(to); + } else { + pushState(to); + } + onNavigateCallback(createLocationObject()); + return () => {}; + }, []); + + return null; +} diff --git a/src/js/src/index.ts b/src/js/src/index.ts index d7c6b3e..8de0626 100644 --- a/src/js/src/index.ts +++ b/src/js/src/index.ts @@ -1,129 +1 @@ -import React from "preact/compat"; -import ReactDOM from "preact/compat"; -import { createLocationObject, pushState, replaceState } from "./utils"; -import { - HistoryProps, - LinkProps, - NavigateProps, - FirstLoadProps, -} from "./types"; - -/** - * 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), - }; -} - -/** - * History component that captures browser "history go back" actions and notifies the server. - */ -export function History({ onHistoryChangeCallback }: HistoryProps): null { - React.useEffect(() => { - // Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback. - const listener = () => { - onHistoryChangeCallback(createLocationObject()); - }; - - // Register the event listener - window.addEventListener("popstate", listener); - - // Delete the event listener when the component is unmounted - return () => window.removeEventListener("popstate", listener); - }); - - // Tell the server about the URL during the initial page load - // FIXME: This code is commented out since it currently runs every time any component - // is mounted due to a ReactPy core rendering bug. `FirstLoad` component is used instead. - // https://github.com/reactive-python/reactpy/pull/1224 - // React.useEffect(() => { - // onHistoryChange({ - // pathname: window.location.pathname, - // search: window.location.search, - // }); - // return () => {}; - // }, []); - return null; -} - -/** - * Link component that captures clicks on anchor links and notifies the server. - * - * This component is not the actual `` link element. It is just an event - * listener for ReactPy-Router's server-side link component. - * - * @disabled This component is currently unused due to a ReactPy core rendering bug - * which causes duplicate rendering (and thus duplicate event listeners). - */ -export function Link({ onClickCallback, linkClass }: LinkProps): null { - React.useEffect(() => { - // Event function that will tell the server about clicks - const handleClick = (event: MouseEvent) => { - event.preventDefault(); - let to = (event.target as HTMLElement).getAttribute("href"); - pushState(to); - onClickCallback(createLocationObject()); - }; - - // Register the event listener - let link = document.querySelector(`.${linkClass}`); - if (link) { - link.addEventListener("click", handleClick); - } else { - console.warn(`Link component with class name ${linkClass} not found.`); - } - - // Delete the event listener when the component is unmounted - return () => { - let link = document.querySelector(`.${linkClass}`); - if (link) { - link.removeEventListener("click", handleClick); - } - }; - }); - return null; -} - -/** - * Client-side portion of the navigate component, that allows the server to command the client to change URLs. - */ -export function Navigate({ - onNavigateCallback, - to, - replace = false, -}: NavigateProps): null { - React.useEffect(() => { - if (replace) { - replaceState(to); - } else { - pushState(to); - } - onNavigateCallback(createLocationObject()); - return () => {}; - }, []); - - return null; -} - -/** - * FirstLoad component that captures the URL during the initial page load and notifies the server. - * - * FIXME: This component only exists because of a ReactPy core rendering bug, and should be removed when the bug - * is fixed. In the future, all this logic should be handled by the `History` component. - * https://github.com/reactive-python/reactpy/pull/1224 - */ -export function FirstLoad({ onFirstLoadCallback }: FirstLoadProps): null { - React.useEffect(() => { - onFirstLoadCallback(createLocationObject()); - return () => {}; - }, []); - - return null; -} +export { bind, History, Link, Navigate } from "./components"; diff --git a/src/js/src/types.ts b/src/js/src/types.ts index f4cf6cd..7144668 100644 --- a/src/js/src/types.ts +++ b/src/js/src/types.ts @@ -17,7 +17,3 @@ export interface NavigateProps { to: string; replace?: boolean; } - -export interface FirstLoadProps { - onFirstLoadCallback: (location: ReactPyLocation) => void; -} diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts index a0d1af7..e3f1dd5 100644 --- a/src/js/src/utils.ts +++ b/src/js/src/utils.ts @@ -7,10 +7,18 @@ export function createLocationObject(): ReactPyLocation { }; } -export function pushState(to: string): void { +export function pushState(to: any): void { + if (typeof to !== "string") { + console.error("pushState() requires a string argument."); + return; + } window.history.pushState(null, "", new URL(to, window.location.href)); } -export function replaceState(to: string): void { +export function replaceState(to: any): void { + if (typeof to !== "string") { + console.error("replaceState() requires a string argument."); + return; + } window.history.replaceState(null, "", new URL(to, window.location.href)); } diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py index 6a84799..6a751e7 100644 --- a/src/reactpy_router/components.py +++ b/src/reactpy_router/components.py @@ -2,10 +2,9 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from urllib.parse import urljoin from uuid import uuid4 -from reactpy import component, html, use_connection +from reactpy import component, html, use_connection, use_ref from reactpy.backend.types import Location from reactpy.web.module import export, module_from_file @@ -34,13 +33,6 @@ ) """Client-side portion of the navigate component""" -FirstLoad = export( - module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), - ("FirstLoad"), -) - -link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8") - def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) -> Component: """ @@ -59,8 +51,7 @@ def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) -> @component def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: attributes = attributes.copy() - uuid_string = f"link-{uuid4().hex}" - class_name = f"{uuid_string}" + class_name = use_ref(f"link-{uuid4().hex}").current set_location = _use_route_state().set_location if "className" in attributes: class_name = " ".join([attributes.pop("className"), class_name]) @@ -80,44 +71,10 @@ def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: "className": class_name, } - # FIXME: This component currently works in a "dumb" way by trusting that ReactPy's script tag \ - # properly sets the location due to bugs in ReactPy rendering. - # https://github.com/reactive-python/reactpy/pull/1224 - current_path = use_connection().location.pathname - - def on_click(_event: dict[str, Any]) -> None: - if _event.get("ctrlKey", False): - return - - pathname, search = to.split("?", 1) if "?" in to else (to, "") - if search: - search = f"?{search}" - - # Resolve relative paths that match `../foo` - if pathname.startswith("../"): - pathname = urljoin(current_path, pathname) - - # Resolve relative paths that match `foo` - if not pathname.startswith("/"): - pathname = urljoin(current_path, pathname) - - # Resolve relative paths that match `/foo/../bar` - while "/../" in pathname: - part_1, part_2 = pathname.split("/../", 1) - pathname = urljoin(f"{part_1}/", f"../{part_2}") - - # Resolve relative paths that match `foo/./bar` - pathname = pathname.replace("/./", "/") - - set_location(Location(pathname, search)) - - attrs["onClick"] = on_click - - return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string))) + def on_click_callback(_event: dict[str, Any]) -> None: + set_location(Location(**_event)) - # def on_click_callback(_event: dict[str, Any]) -> None: - # set_location(Location(**_event)) - # return html._(html.a(attrs, *children), Link({"onClickCallback": on_click_callback, "linkClass": uuid_string})) + return html._(Link({"onClickCallback": on_click_callback, "linkClass": class_name}), html.a(attrs, *children)) def route(path: str, element: Any | None, *routes: Route) -> Route: diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index d8e75f2..25c37b4 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -7,11 +7,11 @@ from typing import TYPE_CHECKING, Any, Literal, cast from reactpy import component, use_memo, use_state -from reactpy.backend.hooks import ConnectionContext, use_connection from reactpy.backend.types import Connection, Location +from reactpy.core.hooks import ConnectionContext, use_connection from reactpy.types import ComponentType, VdomDict -from reactpy_router.components import FirstLoad, History +from reactpy_router.components import History from reactpy_router.hooks import RouteState, _route_state_context from reactpy_router.resolvers import StarletteResolver @@ -20,16 +20,16 @@ from reactpy.core.component import Component - from reactpy_router.types import CompiledRoute, Resolver, Router, RouteType + from reactpy_router.types import CompiledRoute, Resolver, Route, Router __all__ = ["browser_router", "create_router"] _logger = getLogger(__name__) -def create_router(resolver: Resolver[RouteType]) -> Router[RouteType]: +def create_router(resolver: Resolver[Route]) -> Router[Route]: """A decorator that turns a resolver into a router""" - def wrapper(*routes: RouteType) -> Component: + def wrapper(*routes: Route) -> Component: return router(*routes, resolver=resolver) return wrapper @@ -38,13 +38,13 @@ def wrapper(*routes: RouteType) -> Component: _starlette_router = create_router(StarletteResolver) -def browser_router(*routes: RouteType) -> Component: +def browser_router(*routes: Route) -> Component: """This is the recommended router for all ReactPy-Router web projects. It uses the JavaScript [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) to manage the history stack. Args: - *routes (RouteType): A list of routes to be rendered by the router. + *routes (Route): A list of routes to be rendered by the router. Returns: A router component that renders the given routes. @@ -54,8 +54,8 @@ def browser_router(*routes: RouteType) -> Component: @component def router( - *routes: RouteType, - resolver: Resolver[RouteType], + *routes: Route, + resolver: Resolver[Route], ) -> VdomDict | None: """A component that renders matching route(s) using the given resolver. @@ -76,9 +76,9 @@ def router( if match: if first_load: # We need skip rendering the application on 'first_load' to avoid - # rendering it twice. The second render occurs following - # the impending on_history_change event + # rendering it twice. The second render follows the on_history_change event route_elements = [] + set_first_load(False) else: route_elements = [ _route_state_context( @@ -94,15 +94,8 @@ def on_history_change(event: dict[str, Any]) -> None: if location != new_location: set_location(new_location) - def on_first_load(event: dict[str, Any]) -> None: - """Callback function used within the JavaScript `FirstLoad` component.""" - if first_load: - set_first_load(False) - on_history_change(event) - return ConnectionContext( History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value] - FirstLoad({"onFirstLoadCallback": on_first_load}) if first_load else "", *route_elements, value=Connection(old_conn.scope, location, old_conn.carrier), ) @@ -110,7 +103,7 @@ def on_first_load(event: dict[str, Any]) -> None: return None -def _iter_routes(routes: Sequence[RouteType]) -> Iterator[RouteType]: +def _iter_routes(routes: Sequence[Route]) -> Iterator[Route]: for parent in routes: for child in _iter_routes(parent.routes): yield replace(child, path=parent.path + child.path) # type: ignore[misc] diff --git a/src/reactpy_router/static/link.js b/src/reactpy_router/static/link.js deleted file mode 100644 index 7ab069b..0000000 --- a/src/reactpy_router/static/link.js +++ /dev/null @@ -1,17 +0,0 @@ -document.querySelector(".UUID").addEventListener( - "click", - (event) => { - // Prevent default if ctrl isn't pressed - if (!event.ctrlKey) { - event.preventDefault(); - let to = event.currentTarget.getAttribute("href"); - let new_url = new URL(to, window.location); - - // Deduplication needed due to ReactPy rendering bug - if (new_url.href !== window.location.href) { - window.history.pushState(null, "", new URL(to, window.location)); - } - } - }, - { once: true }, -); diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index 81404b7..ca2c913 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -46,9 +46,6 @@ def __hash__(self) -> int: return hash((self.path, key, self.routes)) -RouteType = TypeVar("RouteType", bound=Route) -"""A type variable for `Route`.""" - RouteType_contra = TypeVar("RouteType_contra", bound=Route, contravariant=True) """A contravariant type variable for `Route`.""" @@ -66,6 +63,7 @@ def __call__(self, *routes: RouteType_contra) -> Component: Returns: The resulting component after processing the routes. """ + ... class Resolver(Protocol[RouteType_contra]): @@ -81,6 +79,7 @@ def __call__(self, route: RouteType_contra) -> CompiledRoute: Returns: The compiled route. """ + ... class CompiledRoute(Protocol): @@ -104,6 +103,7 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: Returns: A tuple containing the associated element and a dictionary of path parameters, or None if the path cannot be resolved. """ + ... class ConversionInfo(TypedDict): From 88ec72faa8572c308baa4fd25464763fdd74c20c Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sun, 12 Jan 2025 03:07:08 -0800 Subject: [PATCH 22/22] Custom router API (#51) --- CHANGELOG.md | 10 +++ .../python/custom_router_easy_resolver.py | 16 +++++ .../python/custom_router_easy_router.py | 6 ++ docs/examples/python/example/__init__.py | 0 docs/examples/python/example/resolvers.py | 4 ++ docs/mkdocs.yml | 2 +- docs/src/learn/custom-router.md | 29 +++++++- src/reactpy_router/resolvers.py | 30 ++++---- src/reactpy_router/routers.py | 71 +++++++------------ src/reactpy_router/types.py | 36 +++++++--- tests/test_resolver.py | 27 +++++-- 11 files changed, 150 insertions(+), 81 deletions(-) create mode 100644 docs/examples/python/custom_router_easy_resolver.py create mode 100644 docs/examples/python/custom_router_easy_router.py create mode 100644 docs/examples/python/example/__init__.py create mode 100644 docs/examples/python/example/resolvers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fced87..7fc4f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,22 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] +### Added + +- Support for custom routers. + ### Changed - Set maximum ReactPy version to `<2.0.0`. - Set minimum ReactPy version to `1.1.0`. - `link` element now calculates URL changes using the client. - Refactoring related to `reactpy>=1.1.0` changes. +- Changed ReactPy-Router's method of waiting for the initial URL to be deterministic. +- Rename `StarletteResolver` to `ReactPyResolver`. + +### Removed + +- `StarletteResolver` is removed in favor of `ReactPyResolver`. ### Fixed diff --git a/docs/examples/python/custom_router_easy_resolver.py b/docs/examples/python/custom_router_easy_resolver.py new file mode 100644 index 0000000..322cce3 --- /dev/null +++ b/docs/examples/python/custom_router_easy_resolver.py @@ -0,0 +1,16 @@ +from typing import ClassVar + +from reactpy_router.resolvers import ConversionInfo, ReactPyResolver + + +# Create a custom resolver that uses the following pattern: "{name:type}" +class CustomResolver(ReactPyResolver): + # Match parameters that use the "" format + param_pattern: str = r"<(?P\w+)(?P:\w+)?>" + + # Enable matching for the following types: int, str, any + converters: ClassVar[dict[str, ConversionInfo]] = { + "int": ConversionInfo(regex=r"\d+", func=int), + "str": ConversionInfo(regex=r"[^/]+", func=str), + "any": ConversionInfo(regex=r".*", func=str), + } diff --git a/docs/examples/python/custom_router_easy_router.py b/docs/examples/python/custom_router_easy_router.py new file mode 100644 index 0000000..7457138 --- /dev/null +++ b/docs/examples/python/custom_router_easy_router.py @@ -0,0 +1,6 @@ +from example.resolvers import CustomResolver + +from reactpy_router.routers import create_router + +# This can be used in any location where `browser_router` was previously used +custom_router = create_router(CustomResolver) diff --git a/docs/examples/python/example/__init__.py b/docs/examples/python/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/examples/python/example/resolvers.py b/docs/examples/python/example/resolvers.py new file mode 100644 index 0000000..ad328cb --- /dev/null +++ b/docs/examples/python/example/resolvers.py @@ -0,0 +1,4 @@ +from reactpy_router.resolvers import ReactPyResolver + + +class CustomResolver(ReactPyResolver): ... diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 28df470..ad4ab0f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -6,7 +6,7 @@ nav: - Advanced Topics: - Routers, Routes, and Links: learn/routers-routes-and-links.md - Hooks: learn/hooks.md - - Creating a Custom Router 🚧: learn/custom-router.md + - Creating a Custom Router: learn/custom-router.md - Reference: - Routers: reference/routers.md - Components: reference/components.md diff --git a/docs/src/learn/custom-router.md b/docs/src/learn/custom-router.md index fa03675..c0b1bac 100644 --- a/docs/src/learn/custom-router.md +++ b/docs/src/learn/custom-router.md @@ -1,3 +1,28 @@ -# Custom Router +Custom routers can be used to define custom routing logic for your application. This is useful when you need to implement a custom routing algorithm or when you need to integrate with an existing URL routing system. -Under construction 🚧 +--- + +## Step 1: Creating a custom resolver + +You may want to create a custom resolver to allow ReactPy to utilize an existing routing syntax. + +To start off, you will need to create a subclass of `#!python ReactPyResolver`. Within this subclass, you have two attributes which you can modify to support your custom routing syntax: + +- `#!python param_pattern`: A regular expression pattern that matches the parameters in your URL. This pattern must contain the regex named groups `name` and `type`. +- `#!python converters`: A dictionary that maps a `type` to it's respective `regex` pattern and a converter `func`. + +=== "resolver.py" + + ```python + {% include "../../examples/python/custom_router_easy_resolver.py" %} + ``` + +## Step 2: Creating a custom router + +Then, you can use this resolver to create your custom router... + +=== "resolver.py" + + ```python + {% include "../../examples/python/custom_router_easy_router.py" %} + ``` diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py index 48de28f..58e7b7f 100644 --- a/src/reactpy_router/resolvers.py +++ b/src/reactpy_router/resolvers.py @@ -1,31 +1,29 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, ClassVar from reactpy_router.converters import CONVERTERS +from reactpy_router.types import MatchedRoute if TYPE_CHECKING: from reactpy_router.types import ConversionInfo, ConverterMapping, Route -__all__ = ["StarletteResolver"] +__all__ = ["ReactPyResolver"] -class StarletteResolver: - """URL resolver that matches routes using starlette's URL routing syntax. +class ReactPyResolver: + """URL resolver that can match a path against any given routes. - However, this resolver adds a few additional parameter types on top of Starlette's syntax.""" + URL routing syntax for this resolver is based on Starlette, and supports a mixture of Starlette and Django parameter types.""" - def __init__( - self, - route: Route, - param_pattern=r"{(?P\w+)(?P:\w+)?}", - converters: dict[str, ConversionInfo] | None = None, - ) -> None: + param_pattern: str = r"{(?P\w+)(?P:\w+)?}" + converters: ClassVar[dict[str, ConversionInfo]] = CONVERTERS + + def __init__(self, route: Route) -> None: self.element = route.element - self.registered_converters = converters or CONVERTERS self.converter_mapping: ConverterMapping = {} - self.param_regex = re.compile(param_pattern) + self.param_regex = re.compile(self.param_pattern) self.pattern = self.parse_path(route.path) self.key = self.pattern.pattern # Unique identifier for ReactPy rendering @@ -48,7 +46,7 @@ def parse_path(self, path: str) -> re.Pattern[str]: # Check if a converter exists for the type try: - conversion_info = self.registered_converters[param_type] + conversion_info = self.converters[param_type] except KeyError as e: msg = f"Unknown conversion type {param_type!r} in {path!r}" raise ValueError(msg) from e @@ -70,7 +68,7 @@ def parse_path(self, path: str) -> re.Pattern[str]: return re.compile(pattern) - def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: + def resolve(self, path: str) -> MatchedRoute | None: match = self.pattern.match(path) if match: # Convert the matched groups to the correct types @@ -80,5 +78,5 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: else parameter_name: self.converter_mapping[parameter_name](value) for parameter_name, value in match.groupdict().items() } - return (self.element, params) + return MatchedRoute(self.element, params, path) return None diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index 25c37b4..fad94eb 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -4,7 +4,7 @@ from dataclasses import replace from logging import getLogger -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, Union, cast from reactpy import component, use_memo, use_state from reactpy.backend.types import Connection, Location @@ -13,14 +13,14 @@ from reactpy_router.components import History from reactpy_router.hooks import RouteState, _route_state_context -from reactpy_router.resolvers import StarletteResolver +from reactpy_router.resolvers import ReactPyResolver if TYPE_CHECKING: from collections.abc import Iterator, Sequence from reactpy.core.component import Component - from reactpy_router.types import CompiledRoute, Resolver, Route, Router + from reactpy_router.types import CompiledRoute, MatchedRoute, Resolver, Route, Router __all__ = ["browser_router", "create_router"] _logger = getLogger(__name__) @@ -35,7 +35,7 @@ def wrapper(*routes: Route) -> Component: return wrapper -_starlette_router = create_router(StarletteResolver) +_router = create_router(ReactPyResolver) def browser_router(*routes: Route) -> Component: @@ -49,7 +49,7 @@ def browser_router(*routes: Route) -> Component: Returns: A router component that renders the given routes. """ - return _starlette_router(*routes) + return _router(*routes) @component @@ -57,36 +57,27 @@ def router( *routes: Route, resolver: Resolver[Route], ) -> VdomDict | None: - """A component that renders matching route(s) using the given resolver. + """A component that renders matching route using the given resolver. - This typically should never be used by a user. Instead, use `create_router` if creating + User notice: This component typically should never be used. Instead, use `create_router` if creating a custom routing engine.""" - old_conn = use_connection() - location, set_location = use_state(old_conn.location) - first_load, set_first_load = use_state(True) - + old_connection = use_connection() + location, set_location = use_state(cast(Union[Location, None], None)) resolvers = use_memo( lambda: tuple(map(resolver, _iter_routes(routes))), dependencies=(resolver, hash(routes)), ) - - match = use_memo(lambda: _match_route(resolvers, location, select="first")) + route_element = None + match = use_memo(lambda: _match_route(resolvers, location or old_connection.location)) if match: - if first_load: - # We need skip rendering the application on 'first_load' to avoid - # rendering it twice. The second render follows the on_history_change event - route_elements = [] - set_first_load(False) - else: - route_elements = [ - _route_state_context( - element, - value=RouteState(set_location, params), - ) - for element, params in match - ] + # Skip rendering until ReactPy-Router knows what URL the page is on. + if location: + route_element = _route_state_context( + match.element, + value=RouteState(set_location, match.params), + ) def on_history_change(event: dict[str, Any]) -> None: """Callback function used within the JavaScript `History` component.""" @@ -96,8 +87,8 @@ def on_history_change(event: dict[str, Any]) -> None: return ConnectionContext( History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value] - *route_elements, - value=Connection(old_conn.scope, location, old_conn.carrier), + route_element, + value=Connection(old_connection.scope, location or old_connection.location, old_connection.carrier), ) return None @@ -110,9 +101,9 @@ def _iter_routes(routes: Sequence[Route]) -> Iterator[Route]: yield parent -def _add_route_key(match: tuple[Any, dict[str, Any]], key: str | int) -> Any: +def _add_route_key(match: MatchedRoute, key: str | int) -> Any: """Add a key to the VDOM or component on the current route, if it doesn't already have one.""" - element, _params = match + element = match.element if hasattr(element, "render") and not element.key: element = cast(ComponentType, element) element.key = key @@ -125,24 +116,12 @@ def _add_route_key(match: tuple[Any, dict[str, Any]], key: str | int) -> Any: def _match_route( compiled_routes: Sequence[CompiledRoute], location: Location, - select: Literal["first", "all"], -) -> list[tuple[Any, dict[str, Any]]]: - matches = [] - +) -> MatchedRoute | None: for resolver in compiled_routes: match = resolver.resolve(location.pathname) if match is not None: - if select == "first": - return [_add_route_key(match, resolver.key)] + return _add_route_key(match, resolver.key) - # Matching multiple routes is disabled since `react-router` no longer supports multiple - # matches via the `Route` component. However, it's kept here to support future changes - # or third-party routers. - # TODO: The `resolver.key` value has edge cases where it is not unique enough to use as - # a key here. We can potentially fix this by throwing errors for duplicate identical routes. - matches.append(_add_route_key(match, resolver.key)) # pragma: no cover + _logger.debug("No matching route found for %s", location.pathname) - if not matches: - _logger.debug("No matching route found for %s", location.pathname) - - return matches + return None diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index ca2c913..755e244 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -28,9 +28,9 @@ class Route: A class representing a route that can be matched against a path. Attributes: - path (str): The path to match against. - element (Any): The element to render if the path matches. - routes (Sequence[Self]): Child routes. + path: The path to match against. + element: The element to render if the path matches. + routes: Child routes. Methods: __hash__() -> int: Returns a hash value for the route based on its path, element, and child routes. @@ -67,11 +67,11 @@ def __call__(self, *routes: RouteType_contra) -> Component: class Resolver(Protocol[RouteType_contra]): - """Compile a route into a resolver that can be matched against a given path.""" + """A class, that when instantiated, can match routes against a given path.""" def __call__(self, route: RouteType_contra) -> CompiledRoute: """ - Compile a route into a resolver that can be matched against a given path. + Compile a route into a resolver that can be match routes against a given path. Args: route: The route to compile. @@ -87,18 +87,18 @@ class CompiledRoute(Protocol): A protocol for a compiled route that can be matched against a path. Attributes: - key (Key): A property that uniquely identifies this resolver. + key: A property that uniquely identifies this resolver. """ @property def key(self) -> Key: ... - def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: + def resolve(self, path: str) -> MatchedRoute | None: """ Return the path's associated element and path parameters or None. Args: - path (str): The path to resolve. + path: The path to resolve. Returns: A tuple containing the associated element and a dictionary of path parameters, or None if the path cannot be resolved. @@ -106,13 +106,29 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: ... +@dataclass(frozen=True) +class MatchedRoute: + """ + Represents a matched route. + + Attributes: + element: The element to render. + params: The parameters extracted from the path. + path: The path that was matched. + """ + + element: Any + params: dict[str, Any] + path: str + + class ConversionInfo(TypedDict): """ A TypedDict that holds information about a conversion type. Attributes: - regex (str): The regex to match the conversion type. - func (ConversionFunc): The function to convert the matched string to the expected type. + regex: The regex to match the conversion type. + func: The function to convert the matched string to the expected type. """ regex: str diff --git a/tests/test_resolver.py b/tests/test_resolver.py index a5dad38..cf1a17e 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -4,18 +4,33 @@ import pytest from reactpy_router import route -from reactpy_router.resolvers import StarletteResolver +from reactpy_router.resolvers import ReactPyResolver +from reactpy_router.types import MatchedRoute def test_resolve_any(): - resolver = StarletteResolver(route("{404:any}", "Hello World")) + resolver = ReactPyResolver(route("{404:any}", "Hello World")) assert resolver.parse_path("{404:any}") == re.compile("^(?P<_numeric_404>.*)$") assert resolver.converter_mapping == {"_numeric_404": str} - assert resolver.resolve("/hello/world") == ("Hello World", {"404": "/hello/world"}) + assert resolver.resolve("/hello/world") == MatchedRoute( + element="Hello World", params={"404": "/hello/world"}, path="/hello/world" + ) + + +def test_custom_resolver(): + class CustomResolver(ReactPyResolver): + param_pattern = r"<(?P\w+)(?P:\w+)?>" + + resolver = CustomResolver(route("<404:any>", "Hello World")) + assert resolver.parse_path("<404:any>") == re.compile("^(?P<_numeric_404>.*)$") + assert resolver.converter_mapping == {"_numeric_404": str} + assert resolver.resolve("/hello/world") == MatchedRoute( + element="Hello World", params={"404": "/hello/world"}, path="/hello/world" + ) def test_parse_path(): - resolver = StarletteResolver(route("/", None)) + resolver = ReactPyResolver(route("/", None)) assert resolver.parse_path("/a/b/c") == re.compile("^/a/b/c$") assert resolver.converter_mapping == {} @@ -45,13 +60,13 @@ def test_parse_path(): def test_parse_path_unkown_conversion(): - resolver = StarletteResolver(route("/", None)) + resolver = ReactPyResolver(route("/", None)) with pytest.raises(ValueError, match="Unknown conversion type 'unknown' in '/a/{b:unknown}/c'"): resolver.parse_path("/a/{b:unknown}/c") def test_parse_path_re_escape(): """Check that we escape regex characters in the path""" - resolver = StarletteResolver(route("/", None)) + resolver = ReactPyResolver(route("/", None)) assert resolver.parse_path("/a/{b:int}/c.d") == re.compile(r"^/a/(?P\d+)/c\.d$") assert resolver.converter_mapping == {"b": int}