diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 6a636af..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow 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: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - 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: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b43633 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,121 @@ +name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI + +on: push + +jobs: + build: + name: Build distribution 📦 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: | + setup.py + tox.ini + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v3 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: >- + Publish Python 🐍 distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags/') + needs: + - build + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/p/django-svelte + permissions: + id-token: write + + steps: + - name: Download all the dists + uses: actions/download-artifact@v3 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + name: >- + Sign the Python 🐍 distribution 📦 with Sigstore + and upload them to GitHub Release + needs: + - publish-to-pypi + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore + + steps: + - name: Download all the dists + uses: actions/download-artifact@v3 + with: + name: python-package-distributions + path: dist/ + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v1.2.3 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release create + '${{ github.ref_name }}' + --repo '${{ github.repository }}' + --notes "" + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + '${{ github.ref_name }}' dist/** + --repo '${{ github.repository }}' + + # publish-to-testpypi: + # name: Publish Python 🐍 distribution 📦 to TestPyPI + # needs: + # - build + # runs-on: ubuntu-latest + + # environment: + # name: testpypi + # url: https://test.pypi.org/p/django-svelte + + # permissions: + # id-token: write # IMPORTANT: mandatory for trusted publishing + + # steps: + # - name: Download all the dists + # uses: actions/download-artifact@v3 + # with: + # name: python-package-distributions + # path: dist/ + # - name: Publish distribution 📦 to TestPyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a537741 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Test + +on: + - push + - pull_request + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + setup.py + tox.ini + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox -v + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd72e4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +**/__pycache__/* +**/node_modules/* +*.pyc +*.sqlite3 +*.log +*.final +*.egg-info/* + +.coverage +coverage.xml +.tox diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f51b4b3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## Version 0.2.0 +### Added +* This changelog! +* Demo project into this package illustrating working Svelte 4 deployment. +* Unittests! +* Setting `DJANGO_SVELTE_VITE_MANIFEST_PATH` which locates the Vite build manifest so that hashed files can be looked up. +* Setting `DJANGO_SVELTE_ENTRYPOINT_PREFIX` with default (`src/`) to indicate the directory of the Svelte entrypoint files (formerly referred to like `main-.js`, now format like `.js` is preferred) +* Setting `DJANGO_SVELTE_VITE_ASSETSDIR` with default (`assets/`) to indicate the directory where Vite will output the various bundles for reference. +* Support for looking up hashed file names from the Vite manifest. +### Changed +* Handling of CSS to dedicated templatetag which can appropriately be invoked in the `head` of the template. +* Documentation to speak more directly to handling CSS in the two most common use cases. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd8b784 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +TEST_PATH = "" + +migrations: + cd demo_project/django_svelte_demo && python manage.py makemigrations +init: + cd demo_project/django_svelte_demo && python manage.py migrate +run: + cd demo_project/django_svelte_demo && python manage.py runserver 0.0.0.0:8000 +lint: + black django_svelte + isort django_svelte + cd django_svelte && pflake8 +test: + DJANGO_SETTINGS_MODULE=django_svelte_demo.settings.test pytest --cov=django_svelte --cov-report term-missing tests/$(TEST_PATH) diff --git a/README.md b/README.md index c6ee6f8..17235f2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,23 @@ # Django Svelte +[![PyPI](https://img.shields.io/pypi/v/django-svelte?color=156741&logo=python&logoColor=ffffff&style=for-the-badge)](https://pypi.org/project/django-svelte/) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/thismatters/django-svelte/test.yml?branch=main&color=156741&label=CI&logo=github&style=for-the-badge)](https://github.com/thismatters/django-svelte/actions) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-svelte?color=156741&logo=python&logoColor=white&style=for-the-badge)](https://pypi.org/project/django-svelte/) +[![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-svelte?color=156741&logo=django&logoColor=ffffff&style=for-the-badge)](https://pypi.org/project/django-svelte/) +[![Codecov](https://img.shields.io/codecov/c/github/thismatters/django-svelte?color=156741&logo=codecov&logoColor=ffffff&style=for-the-badge)](https://codecov.io/gh/thismatters/django-svelte) + + Incorporate a Svelte frontend into a Django site with minimal impact to deployment strategy and authentication story. ## Scope -This package mainly consists of a templatetag which facilitates the import of the js/css bundle created by Svelte/Rollup/Node.js into your template. For this package to be useful you will also need the Svelte/Rollup/Node.js which produces the js/css bundle; consider using the accompanying project [django-svelte-template](https://github.com/thismatters/django-svelte-template/) as a starting point for your Svelte frontend. It has been modified to work easily alongside this package. If you run into any problems see the [django-svelte-demo](https://github.com/thismatters/django-svelte-demo) for an example of these two projects working together. +This package mainly consists of a templatetags which facilitate the import of JS/CSS bundles created by Svelte/Vite/Node.js into your template. For this package to be useful you will also need the Svelte/Vite/Node.js which produces the JS/CSS bundle. The [svelte demo project](demo_project/svelte/) walks you through setting this up from scratch. + +## Compatibility + +This package was originally written during the Svelte 3 days and was bundled by Rollup. Version 0.1.7 and prior were devoted to supporting this stack. Newer versions of this package _ought_ to support that old stack, but they are untested. + +Version 0.2.0 and on support Svelte 4 bundled by Vite. ## Installation @@ -24,39 +37,137 @@ INSTALLED_APPS = ( ) ``` -Tell Django where your Svelte js/css bundles will be found (this guide assumes that you place your svelte directory beside your django project directory): +Tell Django where your Svelte JS/CSS bundles will be found (this guide assumes that you place your svelte directory beside your django project directory as shown in `demo_project`): ```py STATICFILES_DIRS = [ - BASE_DIR.parent / "svelte" / "public" / "build", + # ... + BASE_DIR.parent / "svelte" / "dist" / "assets", ] ``` +If you are using Vite as the bundler for Svelte then you'll also want to tell Django where to find the Vite manifest so that components can be located by hash (Vite does not generate a manifest by default, see the [`build.manifest` option](https://vitejs.dev/config/build-options.html#build-manifest)): + +```py +DJANGO_SVELTE_VITE_MANIFEST_PATH = BASE_DIR.parent / "svelte" / "dist" / ".vite" / "manifest.json" , +``` + ## Usage -To use a Svelte component within your Django template load the `django_svelte` templatetag library and use the `display_svelte` templatetag: +To use a Svelte component within your Django template load the `django_svelte` templatetag library and use the `display_svelte` and `display_svelte_css` templatetag: ``` {% load django_svelte %} ... -{% display_svelte "MySpecialComponent.svelte" %} +{% display_svelte_css "MySpecialComponent" %} + +{% display_svelte "MySpecialComponent" %} +``` + +You can optionally pass some context (specifically a JSON serializable `dict`) to the component: + +``` +{% display_svelte "MySpecialComponent" component_props %} ``` -You can optionally pass some context (specifically a `dict`) to the component: +Versions prior to 0.2.0 required using `.svelte` at the end of the component name, e.g. ``` {% display_svelte "MySpecialComponent.svelte" component_props %} ``` -## What about the Svelte!? +This is no longer required. + +## Lets talk about CSS + +There are many ways that Svelte generates CSS bundles. +Because of the flexibility of CSS many arrangements are possible, but they'll require you to update your Django templates accordingly. +Django templates are highly variable per project, so it is difficult to provide one-sized-fits-all advice in this arena. +It is likely that one or both of the use cases described below will work for your project. + +If your project follows the standard `base.html -> site_base.html -> .html` paradigm then you might find it convenient to also provide a `svelte_base.html` which includes a block (namely `svelte_styles`) in the `head` which will allow individual page templates to inject CSS where it ought to exist: + +### Monolithic CSS + +If your project collects all its CSS into a single stylesheet, then you can make that CSS file accessible as a staticfile and use a regular `link` to incorporate it into your template as shown below. It may be convenient to put this import for your stylesheet in your base template to limit boilerplate. + +``` +{% load static %} +{% load django_svelte %} + + + + ... + {% block svelte_styles %} + {{ block.super }} + + ... + {% display_svelte "Component" %} + + +``` + +See also the Vite [build option `cssCodeSplit`](https://vitejs.dev/config/build-options.html#build-csscodesplit) + +### Per Component CSS + +If you include `style` tags in your individual Svelte components then you'll want to include the per component stylesheets when you display your component: + +``` +{% extends "svelte_base.html" %} +{% load django_svelte %} + +{% block svelte_styles %} + {{ block.super }} + {% display_svelte_css "Component" %} +{% endblock %} + + +{% block main_content %} + {# or whatever... #} + {% display_svelte "Component" %} +{% endblock %} +``` + +Note the use of `display_svelte_css` to specifically inject the CSS for the named component within the `svelte_styles` block. + +## Recommended Patterns + +In addition to the `svelte_base.html` template described above, there are some other tips that have proved effective when using this package. Basic implementations of the ideas described below are included with the demo project, but due to the extreme variability of django templates in practice you'll have to provide your own implementations. + +### Class-based Wrapper View + +If you have many components that each get loaded as the main content of their own pages then a reusable class-based view can reduce boilerplate. Subclass the `SvelteTemplateView` class provided in `views.py` to utilize this pattern; be sure to provide your own `get_svelte_props` method so that your component will have data! See the [sample implementation](demo_project/django_svelte_demo/django_svelte_demo/views.py) + +### Default Svelte Template + +Once you have `svelte_base.html` in place, a subsequent template like `svelte_component.html` is a convenient template for loading in a single component. If you're using the class based view approach describe above then this template should include a `{{ page_title }}` as well as the use of `{% display_svelte_css component_name %}` in the `head` of the template, and `{% display_svelte component_name %}` in its body. See the [sample implementation](demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_component.html) in the demo project. + +## What about Svelte!? + +The Svelte side of things is demonstrated in the [Svelte demo project](demo_project/svelte/) which shows how a default Vite+Svelte project can be altered to output JS/CSS bundles useful for this package. It is configured to output JS/CSS bundles for several different components which can be imported independently. + +### New Svelte, who dis? + +The newer versions of Svelte are defaulting to SvelteKit, because routing is important stuff. If you're here then you're probably using Django for your routing, so there is no need for SvelteKit. Fortunately, Svelte (sans-Kit) is still available [by installing `vite`](https://svelte.dev/docs/introduction#start-a-new-project-alternatives-to-sveltekit). + +The config for outputting multiple bundles got a bit easier from prior versions, and I changed the conventions around entrypoints in the demo project. What used to be called `main-.js` is now called `.js` and referenced as such in the `build.rollupOptions.input` array. + +Vite, by default and with much difficulty to alter, hashes files and appends the hash to the filename; probably a good practice, but kind of annoying for integrated packages. Instruct Vite to output a manifest by setting the [`build.manifest` option](https://vitejs.dev/config/build-options.html#build-manifest). The complementary setting `DJANGO_SVELTE_VITE_MANIFEST_PATH` allows you to specify where in the file tree the manifest file lives. After setting this you may go about using the component name (sans hash) in the various templatetags. Alternately, it is possible to prevent Vite from putting hashes into its output filenames but you're on your own for that! + +**NOTE:** This manifest arrangement _will_ get bothersome during dev as you have to cause Django to reload the manifest by saving a Django file each time you save a Svelte file (and the bundle hash changes). If **anybody** knows a way to make this more manageable, please please tell me! + +Depending on how you have organized your Svelte project you may have specify where your Svelte component "Entrypoints" are located (Entrypoints are the thin javascript files that first import the Svelte component, see [App.js](demo_project/svelte/src/App.js). In the demo project, the entrypoints are directly in the `svelte/src/` directory. If you have put your entrypoints somewhere else then you'll need to say where in the `DJANGO_SVELTE_ENTRYPOIN_PREFIX` setting (value needs to be relative to the `svelte` directory and do include the trailing slash, e.g. the default value `src/`). It is necessary that your entrypoints follow the naming convention `.js`; if your top-level Svelte component is called `MySpecialComponent.svelte` then the entrypoint needs to be called `MySpecialComponent.js` or this package will not function correctly! -The Svelte side of things is dealt with in the [django-svelte-template](https://github.com/thismatters/django-svelte-template/) repo which you can use as a starting point for your Svelte projects (using `npx degit thismatters/django-svelte-template svelte`). It is configured to output js/css bundles for several different components, but you'll have to do some setup so be sure to read the README. +It may be necessary to specify the `DJANGO_SVELTE_VITE_ASSETSDIR` setting. It must match the value used in `vite.config.js` under the key `build.assetsDir`. The default case (value `assets/` ) is covered. ## Devops concerns -So, this isn't magic. For this to work you will need to have Node.js _somewhere_ in the mix. Fortunately, you won't need Node.js running in your production environment, but you will need it somewhere in your CI pipeline and probably in your dev environment. For a practical example of what this might look like for a production environment see [django-svelte-demo](https://github.com/thismatters/django-svelte-demo). +So, this package isn't magic. For this to work you will need to have Node.js _somewhere_ in the mix. Fortunately, you won't need Node.js running in your production environment, but you will need it somewhere in your CI pipeline and probably in your dev environment. For a practical example of what this might look like for a production environment see [demo_project](demo_project). ## Shoutouts diff --git a/demo_project/.gitlab-ci.yml b/demo_project/.gitlab-ci.yml new file mode 100644 index 0000000..e1cefa9 --- /dev/null +++ b/demo_project/.gitlab-ci.yml @@ -0,0 +1,25 @@ +stages: + - pre-build + - build + +build-svelte: + stage: pre-build + image: node:current-alpine + script: + - cd svelte + - npm install + - npm run build + artifacts: + paths: + - svelte/dist/ + +build-app: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + script: + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile Dockerfile --destination $CI_REGISTRY_IMAGE:latest + dependencies: + - build-svelte diff --git a/demo_project/Dockerfile b/demo_project/Dockerfile new file mode 100644 index 0000000..2acf2f2 --- /dev/null +++ b/demo_project/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.12-alpine as builder +# RUN adduser -D worker -u 1000 + +RUN apk add --no-cache git + +# Get the python dependencies +COPY requirements.txt /app/ +RUN python -m pip install -r /app/requirements.txt + +# Copy the app itself +COPY django_svelte_demo /app/src +WORKDIR /app/src + +# Get the frontend component staticfiles +COPY svelte/dist /app/svelte/dist + +# Catalog the staticfiles. This is needed in production, but in the dev +# environment the svelte staticfiles will be drawn from +# the svelte/public/build directory +RUN python manage.py collectstatic --noinput + +# Delete the original staticfiles +RUN rm -rf /app/svelte + +# get the runtime instructions +COPY entrypoint.sh /app +RUN chmod +x /app/entrypoint.sh + +# set the runtime user +# USER worker +EXPOSE 8000 +ENTRYPOINT ["sh", "/app/entrypoint.sh"] diff --git a/demo_project/README.md b/demo_project/README.md new file mode 100644 index 0000000..600c744 --- /dev/null +++ b/demo_project/README.md @@ -0,0 +1,17 @@ +# `django-svelte` Demo Project + +This project aims to demonstrate the functionality of the `django-svelte` package. It is intended to illustrate how Node.js can fit into the development and build environments so that it doesn't have to be used in a production environment. + +Pay special attention to the `Dockerfile`, `docker-compose.yml`, and `gitlab-ci.yml` (I'm using Gitlab CI here because it is really great and it is what I use for all my professional projects; if there is enough interest I'll try to replicate the functionality in Github CI). + +## Disclaimer + +This demo probably will run fine, but you're on your own getting it to run. + +## Django Project + +What you'll find is a relatively vanilla Django project, created in the standard `django start-project` way. To it is added Django Rest Framework and this package, `django-svelte`, some templates and views along with the necessary serializers and api views. + +## Svelte Project + +What you'll find is a relatively vanilla Vite+Svelte project, with only a few lines added to the Vite config file. See the [readme within the `svelte` project](svelte/README.md). diff --git a/demo_project/django_svelte_demo/django_svelte_demo/__init__.py b/demo_project/django_svelte_demo/django_svelte_demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo_project/django_svelte_demo/django_svelte_demo/api/urls.py b/demo_project/django_svelte_demo/django_svelte_demo/api/urls.py new file mode 100644 index 0000000..0cfcb28 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/api/urls.py @@ -0,0 +1,28 @@ +from django.urls import path, include +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions + +from django_svelte_demo.api import views + + +schema_view = get_schema_view( + openapi.Info( + title="Django-Svelte Demo API", + default_version="v1", + description="Some test endpoints to verify that auth works right", + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +urlpatterns = [ + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), + path( + "docs/", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path("requires-auth/", views.RequiresAuthAPIView.as_view(), name="requires-auth"), + path("public/", views.PublicAPIView.as_view(), name="requires-auth"), +] diff --git a/demo_project/django_svelte_demo/django_svelte_demo/api/views.py b/demo_project/django_svelte_demo/django_svelte_demo/api/views.py new file mode 100644 index 0000000..c07ff41 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/api/views.py @@ -0,0 +1,19 @@ +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + + +class SgoodMixin: + def get(self, request, *args, **kwargs): + return Response({"sgood": True}) + + def post(self, request, *args, **kwargs): + return Response({"sgood": True, "did_post": True}) + + +class RequiresAuthAPIView(APIView, SgoodMixin): + permission_classes = (IsAuthenticated,) + + +class PublicAPIView(APIView, SgoodMixin): + pass diff --git a/demo_project/django_svelte_demo/django_svelte_demo/asgi.py b/demo_project/django_svelte_demo/django_svelte_demo/asgi.py new file mode 100644 index 0000000..5b75f90 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for django_svelte_demo project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_svelte_demo.settings') + +application = get_asgi_application() diff --git a/demo_project/django_svelte_demo/django_svelte_demo/settings/__init__.py b/demo_project/django_svelte_demo/django_svelte_demo/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo_project/django_svelte_demo/django_svelte_demo/settings/base.py b/demo_project/django_svelte_demo/django_svelte_demo/settings/base.py new file mode 100644 index 0000000..8bd0032 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/settings/base.py @@ -0,0 +1,135 @@ +""" +Django settings for django_svelte_demo project. + +Generated by 'django-admin startproject' using Django 3.1.2. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "^*8@r7lwme4sm9**rxd5g6+q4fbkalgi#1au9o%cow)otux$ok" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["*"] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_svelte", + "rest_framework", + "drf_yasg", + "django_svelte_demo", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "django_svelte_demo.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "django_svelte_demo.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR.parent / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.1/howto/static-files/ + +STATIC_URL = "/static/" + +STATIC_ROOT = BASE_DIR.parent / "site_media" / "static" + +STATICFILES_DIRS = [ + BASE_DIR.parent / "static", + BASE_DIR.parent.parent / "svelte" / "dist" / "assets", +] + +DJANGO_SVELTE_VITE_MANIFEST_PATH = ( + BASE_DIR.parent.parent / "svelte" / "dist" / ".vite" / "manifest.json" +) diff --git a/demo_project/django_svelte_demo/django_svelte_demo/settings/test.py b/demo_project/django_svelte_demo/django_svelte_demo/settings/test.py new file mode 100644 index 0000000..96026c3 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/settings/test.py @@ -0,0 +1,3 @@ +from .base import * # noqa: F401, F403 + +DJANGO_SVELTE_VITE_MANIFEST_PATH = None diff --git a/demo_project/django_svelte_demo/django_svelte_demo/templates/base.html b/demo_project/django_svelte_demo/django_svelte_demo/templates/base.html new file mode 100644 index 0000000..89d2689 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/templates/base.html @@ -0,0 +1,11 @@ + + + {{ page_title|default:"Django Svelte Demo" }} + {% block styles %} + {% endblock %} + + + {% block content %} + {% endblock %} + + diff --git a/demo_project/django_svelte_demo/django_svelte_demo/templates/basic.html b/demo_project/django_svelte_demo/django_svelte_demo/templates/basic.html new file mode 100644 index 0000000..9ccd5fb --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/templates/basic.html @@ -0,0 +1,24 @@ +{% load django_svelte %} + + + + django-svelting change + {% display_svelte_css "App" %} + {% display_svelte_css "AuthComponent" %} + {% display_svelte_css "PostComponent" %} + + + This text is in the django template. Below is the vanilla svelte app with some basic props passed in from the context. +
+ {% display_svelte "App" component_props %} +
+ And we can have multiple different components (they must be different): +
+ {% display_svelte "AuthComponent" %} +
+ And we should be able to POST data as well! +
+ {% display_svelte "PostComponent" %} +
+ + diff --git a/demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_base.html b/demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_base.html new file mode 100644 index 0000000..5c95914 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_base.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} + +{% block styles %} + {{ block.super }} + {% block svelte_styles %} + + {% endblock %} +{% endblock %} diff --git a/demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_component.html b/demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_component.html new file mode 100644 index 0000000..8b313be --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/templates/svelte_component.html @@ -0,0 +1,11 @@ +{% extends "svelte_base.html" %} +{% load django_svelte %} + +{% block svelte_styles %} + {{ block.super }} + {% display_svelte_css component_name %} +{% endblock %} + +{% block content %} + {% display_svelte component_name component_props %} +{% endblock %} \ No newline at end of file diff --git a/demo_project/django_svelte_demo/django_svelte_demo/urls.py b/demo_project/django_svelte_demo/django_svelte_demo/urls.py new file mode 100644 index 0000000..ebaba65 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/urls.py @@ -0,0 +1,55 @@ +"""django_svelte_demo URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +from django.views.generic import TemplateView + +from .views import MySvelteTemplateView, MyContextSvelteTemplateView + +urlpatterns = [ + path( + "", + TemplateView.as_view( + template_name="basic.html", + extra_context={"component_props": {"name": "django-svelte"}}, + ), + name="home", + ), + path( + "single/main/", + MyContextSvelteTemplateView.as_view( + page_title="Main Component", component_name="App" + ), + name="main-component", + ), + path( + "single/auth/", + MySvelteTemplateView.as_view( + page_title="Auth Component", component_name="AuthComponent" + ), + name="auth-component", + ), + path( + "single/post/", + MySvelteTemplateView.as_view( + page_title="Post Component", component_name="PostComponent" + ), + name="post-component", + ), + path("admin/", admin.site.urls), + path("api/v1/", include("django_svelte_demo.api.urls")), +] diff --git a/demo_project/django_svelte_demo/django_svelte_demo/views.py b/demo_project/django_svelte_demo/django_svelte_demo/views.py new file mode 100644 index 0000000..4e43682 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/views.py @@ -0,0 +1,14 @@ +from django_svelte.views import SvelteTemplateView + + +class MySvelteTemplateView(SvelteTemplateView): + template_name = "svelte_component.html" + + def get_svelte_props(self, **kwargs): + return kwargs + + +class MyContextSvelteTemplateView(MySvelteTemplateView): + def get_svelte_props(self, **kwargs): + kwargs.update({"name": "single component view"}) + return kwargs diff --git a/demo_project/django_svelte_demo/django_svelte_demo/wsgi.py b/demo_project/django_svelte_demo/django_svelte_demo/wsgi.py new file mode 100644 index 0000000..2f5aaf8 --- /dev/null +++ b/demo_project/django_svelte_demo/django_svelte_demo/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for django_svelte_demo project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_svelte_demo.settings') + +application = get_wsgi_application() diff --git a/demo_project/django_svelte_demo/manage.py b/demo_project/django_svelte_demo/manage.py new file mode 100755 index 0000000..6441967 --- /dev/null +++ b/demo_project/django_svelte_demo/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_svelte_demo.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/demo_project/django_svelte_demo/static/Hashed-asdfsafd.js b/demo_project/django_svelte_demo/static/Hashed-asdfsafd.js new file mode 100644 index 0000000..7adf3c8 --- /dev/null +++ b/demo_project/django_svelte_demo/static/Hashed-asdfsafd.js @@ -0,0 +1,3 @@ +class Hashed { + +} \ No newline at end of file diff --git a/demo_project/django_svelte_demo/static/Something.css b/demo_project/django_svelte_demo/static/Something.css new file mode 100644 index 0000000..4f79a22 --- /dev/null +++ b/demo_project/django_svelte_demo/static/Something.css @@ -0,0 +1,3 @@ +.base { + text-decoration: underline; +} \ No newline at end of file diff --git a/demo_project/django_svelte_demo/static/Something.js b/demo_project/django_svelte_demo/static/Something.js new file mode 100644 index 0000000..57e1aee --- /dev/null +++ b/demo_project/django_svelte_demo/static/Something.js @@ -0,0 +1,3 @@ +class Something { + +} \ No newline at end of file diff --git a/demo_project/django_svelte_demo/static/fake_manifest.json b/demo_project/django_svelte_demo/static/fake_manifest.json new file mode 100644 index 0000000..8abb56f --- /dev/null +++ b/demo_project/django_svelte_demo/static/fake_manifest.json @@ -0,0 +1,3 @@ +{ + "testkey": "testvalue" +} \ No newline at end of file diff --git a/demo_project/django_svelte_demo/static/real_file.txt b/demo_project/django_svelte_demo/static/real_file.txt new file mode 100644 index 0000000..43a32a1 --- /dev/null +++ b/demo_project/django_svelte_demo/static/real_file.txt @@ -0,0 +1 @@ +This is an actual file! diff --git a/demo_project/django_svelte_demo/static/realistic_manifest.json b/demo_project/django_svelte_demo/static/realistic_manifest.json new file mode 100644 index 0000000..ffbbe59 --- /dev/null +++ b/demo_project/django_svelte_demo/static/realistic_manifest.json @@ -0,0 +1,41 @@ +{ + "src/App.js": { + "css": [ + "assets/App-EY9ns7CU.css" + ], + "file": "assets/App-ivDvN-Hb.js", + "imports": [ + "_index-XiwIJ_QN.js" + ], + "isEntry": true, + "src": "src/App.js" + }, + "src/AuthComponent.js": { + "css": [ + "assets/AuthComponent--tKiZTRp.css" + ], + "file": "assets/AuthComponent-ew7efgSP.js", + "imports": [ + "_index-XiwIJ_QN.js", + "_api-S8e2CqEv.js" + ], + "isEntry": true, + "src": "src/AuthComponent.js" + }, + "src/NoCSS.js": { + "file": "assets/NoCSS-w0j-em_E.js", + "imports": [ + "_index-XiwIJ_QN.js", + "_api-S8e2CqEv.js" + ], + "isEntry": true, + "src": "src/PostComponent.js" + }, + "otherPrefix/FakeComponent.js": { + "file": "otherAssetDir/FakeComponent-asdfasdf.js", + "isEntry": true, + "css": [ + "otherAssetDir/FakeComponent-asdfsdaf.css", + ] + } +} \ No newline at end of file diff --git a/demo_project/docker-compose.yml b/demo_project/docker-compose.yml new file mode 100644 index 0000000..9f5bafb --- /dev/null +++ b/demo_project/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3' + +services: + app: + build: . + environment: + - APP_ENV=dev + ports: + - "8000:8000" + volumes: + - ./django_svelte_demo:/app/src + - ./svelte/dist:/app/svelte/dist/ + depends_on: + - node + networks: + - dev-net + node: + image: node:current-alpine + user: "node" + working_dir: /home/node/app + environment: + - NODE_ENV=development + - API_BASE_URL=localhost:8000 + volumes: + - ./svelte:/home/node/app + command: "npm run dev" + +networks: + dev-net: \ No newline at end of file diff --git a/demo_project/entrypoint.sh b/demo_project/entrypoint.sh new file mode 100644 index 0000000..3b4a731 --- /dev/null +++ b/demo_project/entrypoint.sh @@ -0,0 +1,6 @@ +if [[ $APP_ENV == "dev" ]]; then + python manage.py runserver 0.0.0.0:8000 +else + # do something more production-y + echo "try gunicorn!" +fi \ No newline at end of file diff --git a/demo_project/makefile b/demo_project/makefile new file mode 100644 index 0000000..6751312 --- /dev/null +++ b/demo_project/makefile @@ -0,0 +1,18 @@ +build: + @mkdir -p svelte/public/build/ + @docker compose -p django-svelte-demo build +init: + @docker compose -p django-svelte-demo run app ./manage.py migrate + @docker compose -p django-svelte-demo run node npm install +run: + @docker compose -p django-svelte-demo up -d +stop: + @docker compose -p django-svelte-demo down +applogs: + @docker compose -p django-svelte-demo logs -f app +nodelogs: + @docker compose -p django-svelte-demo logs -f node +shell: + @docker compose -p django-svelte-demo exec app sh +django-shell: + @docker compose -p django-svelte-demo exec app ./manage.py shell diff --git a/demo_project/requirements.txt b/demo_project/requirements.txt new file mode 100644 index 0000000..d1131d0 --- /dev/null +++ b/demo_project/requirements.txt @@ -0,0 +1,18 @@ +Django==5.0.1 + +# demo dependencies +djangorestframework==3.14.0 +drf-yasg==1.21.7 + +# code quality +black==24.1.1 +isort==5.13.2 +pyproject-flake8==6.1.0 + +# basic testing +pytest==8.0.0 +pytest-cov==4.1.0 +pytest-django==4.8.0 + +# matrix testing +tox==4.12.1 \ No newline at end of file diff --git a/demo_project/svelte/.gitignore b/demo_project/svelte/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/demo_project/svelte/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/demo_project/svelte/README.md b/demo_project/svelte/README.md new file mode 100644 index 0000000..e027cca --- /dev/null +++ b/demo_project/svelte/README.md @@ -0,0 +1,24 @@ +# Django Svelte Demo Svelte Project + +This is very nearly a default Vite + Svelte deployment which can be gotten by "running `npm create vite@latest` and selecting the `svelte` option".([docs](https://svelte.dev/docs/introduction#start-a-new-project-alternatives-to-sveltekit)) The changes made to that default project are thus: +* Added components and such to the `src/components` and `src/lib` directories, +* Added entrypoints to the `src/` directory, +* Added the `build` key to the `defineConfig` in `vite.config.js` with the following config: + +```js +export default defineConfig({ + build: { + rollupOptions: { + input: [ + 'src/App.js', + 'src/AuthComponent.js', + 'src/PostComponent.js' + ], // change these out for your own components! + }, + manifest: true, // need this so that Django Svelte can locate hashed files + }, + plugins: [svelte()], +}) +``` + + diff --git a/demo_project/svelte/jsconfig.json b/demo_project/svelte/jsconfig.json new file mode 100644 index 0000000..5696a2d --- /dev/null +++ b/demo_project/svelte/jsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/demo_project/svelte/package-lock.json b/demo_project/svelte/package-lock.json new file mode 100644 index 0000000..fb8b9e5 --- /dev/null +++ b/demo_project/svelte/package-lock.json @@ -0,0 +1,1070 @@ +{ + "name": "svelte-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "svelte-app", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.1", + "svelte": "^4.2.8", + "vite": "^5.0.8" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", + "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", + "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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/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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "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/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.34", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz", + "integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.10.tgz", + "integrity": "sha512-Ep06yCaCdgG1Mafb/Rx8sJ1QS3RW2I2BxGp2Ui9LBHSZ2/tO/aGLc5WqPjgiAP6KAnLJGaIr/zzwQlOo1b8MxA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/vite": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", + "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + } + } +} diff --git a/demo_project/svelte/package.json b/demo_project/svelte/package.json new file mode 100644 index 0000000..9e5f338 --- /dev/null +++ b/demo_project/svelte/package.json @@ -0,0 +1,16 @@ +{ + "name": "svelte-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.1", + "svelte": "^4.2.8", + "vite": "^5.0.8" + } +} diff --git a/demo_project/svelte/src/App.js b/demo_project/svelte/src/App.js new file mode 100644 index 0000000..fd48708 --- /dev/null +++ b/demo_project/svelte/src/App.js @@ -0,0 +1,8 @@ +import App from './components/App.svelte'; + +const app = new App({ + target: document.getElementById("app-target"), + props: JSON.parse(document.getElementById("app-props").textContent), +}); + +export default app; \ No newline at end of file diff --git a/demo_project/svelte/src/AuthComponent.js b/demo_project/svelte/src/AuthComponent.js new file mode 100644 index 0000000..6e426d9 --- /dev/null +++ b/demo_project/svelte/src/AuthComponent.js @@ -0,0 +1,8 @@ +import AuthComponent from './components/AuthComponent.svelte'; + +const authcomponent = new AuthComponent({ + target: document.getElementById("authcomponent-target"), + props: JSON.parse(document.getElementById("authcomponent-props").textContent), +}); + +export default authcomponent; \ No newline at end of file diff --git a/demo_project/svelte/src/PostComponent.js b/demo_project/svelte/src/PostComponent.js new file mode 100644 index 0000000..9c5f908 --- /dev/null +++ b/demo_project/svelte/src/PostComponent.js @@ -0,0 +1,8 @@ +import PostComponent from './components/PostComponent.svelte'; + +const postcomponent = new PostComponent({ + target: document.getElementById("postcomponent-target"), + props: JSON.parse(document.getElementById("postcomponent-props").textContent), +}); + +export default postcomponent; \ No newline at end of file diff --git a/demo_project/svelte/src/components/App.svelte b/demo_project/svelte/src/components/App.svelte new file mode 100644 index 0000000..dbe67f0 --- /dev/null +++ b/demo_project/svelte/src/components/App.svelte @@ -0,0 +1,32 @@ + + +
+

{value} {name}!

+ +

Visit the Svelte tutorial to learn how to build Svelte apps.

+
+ + \ No newline at end of file diff --git a/demo_project/svelte/src/components/AuthComponent.svelte b/demo_project/svelte/src/components/AuthComponent.svelte new file mode 100644 index 0000000..1ab85af --- /dev/null +++ b/demo_project/svelte/src/components/AuthComponent.svelte @@ -0,0 +1,31 @@ + + +

Check your auth status

+ +{#if isAuth} +

You're Auth!

+{:else if didClick} +

You're not Auth!

+{/if} + + + \ No newline at end of file diff --git a/demo_project/svelte/src/components/PostComponent.svelte b/demo_project/svelte/src/components/PostComponent.svelte new file mode 100644 index 0000000..a6346bf --- /dev/null +++ b/demo_project/svelte/src/components/PostComponent.svelte @@ -0,0 +1,19 @@ + + +

POST something. make it good.

+ + + +{#if res} +

{res.sgood ? "Good" : "Bad"} + {res.did_post ? "did post!" : "didn't post :("}

+{/if} \ No newline at end of file diff --git a/demo_project/svelte/src/lib/api.js b/demo_project/svelte/src/lib/api.js new file mode 100644 index 0000000..dc7fc8a --- /dev/null +++ b/demo_project/svelte/src/lib/api.js @@ -0,0 +1,69 @@ +/* + * This is a really basic implementation of a fetch wrapper appropriate + * for use with `django-svelte`. This works for _same origin_ requests, + * where the API backend has the same URL as the frontend. If your setup + * differs then you'll have to build support for that here(ish). + */ + +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +async function send({ method, path, data }) { + const fetch = window.fetch; + const opts = { method, headers: {} }; + + if (data) { + opts.headers['Content-Type'] = 'application/json'; + opts.headers['X-CSRFToken'] = getCookie('csrftoken'); + opts.body = JSON.stringify(data); + } + let url = `/${path}`; + console.log(opts, url); + return await fetch(url, opts) + .then(r => r.text()) + .then(json => { + try { + return JSON.parse(json); + } catch (err) { + console.log(json); + return json; + } + }) + .catch(e => { + console.log(`[${e}]: ${url} with ${JSON.stringify(opts)} failed`); + return {}; + }); +} + +export function get(path) { + return send({ method: 'GET', path }); +} + +export function del(path) { + return send({ method: 'DELETE', path }); +} + +export function post(path, data) { + return send({ method: 'POST', path, data }); +} + +export function put(path, data) { + return send({ method: 'PUT', path, data }); +} + +export function patch(path, data) { + return send({ method: 'PATCH', path, data }); +} diff --git a/demo_project/svelte/src/vite-env.d.ts b/demo_project/svelte/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/demo_project/svelte/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/demo_project/svelte/svelte.config.js b/demo_project/svelte/svelte.config.js new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/demo_project/svelte/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/demo_project/svelte/vite.config.js b/demo_project/svelte/vite.config.js new file mode 100644 index 0000000..9aa4823 --- /dev/null +++ b/demo_project/svelte/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + rollupOptions: { + input: ['src/App.js', 'src/AuthComponent.js', 'src/PostComponent.js'], + }, + manifest: true, + }, + plugins: [svelte()], +}) diff --git a/django_svelte/apps.py b/django_svelte/apps.py new file mode 100644 index 0000000..517bf08 --- /dev/null +++ b/django_svelte/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DjangoSvelteConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "django_svelte" diff --git a/django_svelte/conf.py b/django_svelte/conf.py new file mode 100644 index 0000000..24c0db3 --- /dev/null +++ b/django_svelte/conf.py @@ -0,0 +1,50 @@ +import json + +from appconf import AppConf +from django.conf import settings # noqa: F401 +from django.core.exceptions import ImproperlyConfigured + + +class DjangoSvelteAppConf(AppConf): + VITE_MANIFEST_PATH = None + _VITE_MANIFEST = None + ENTRYPOINT_PREFIX = "src/" + VITE_ASSETSDIR = "assets/" + + def configure__vite_manifest(self, value): + if value is not None: + raise ImproperlyConfigured( + "Do not set the `DJANGO_SVELTE__VITE_MANIFEST` setting, " + "its value is generated!" + ) + return None + + def configure_entrypoint_prefix(self, value): + if value is None: + value = "" + if value and not value.endswith("/"): + raise ImproperlyConfigured( + "Non-blank entrypoint prefix must end with a slash '/'." + ) + return value + + def configure_vite_assetsdir(self, value): + if value is None: + value = "" + if value and not value.endswith("/"): + raise ImproperlyConfigured( + "Non-blank vite assetsdir must end with a slash '/'." + ) + return value + + def configure(self): + """Load the vite manifest into memory""" + _path = self.configured_data["VITE_MANIFEST_PATH"] + if _path is None: + return self.configured_data + if not _path.exists(): + raise ImproperlyConfigured(f"Vite manifest must exist at path! {_path}") + with open(_path) as manifest: + self.configured_data["_VITE_MANIFEST"] = json.load(manifest) + print(self.configured_data["_VITE_MANIFEST"]) + return self.configured_data diff --git a/django_svelte/models.py b/django_svelte/models.py new file mode 100644 index 0000000..f63a48d --- /dev/null +++ b/django_svelte/models.py @@ -0,0 +1,2 @@ +# this is needed to load in the package settings +from django_svelte.conf import settings # noqa: F401 diff --git a/django_svelte/templates/display_svelte.html b/django_svelte/templates/display_svelte.html deleted file mode 100644 index 260bcad..0000000 --- a/django_svelte/templates/display_svelte.html +++ /dev/null @@ -1,16 +0,0 @@ - -{% comment %} -Create a target element -load the compiled bundle -instantiate the bundle with the json context -{% endcomment %} -{% load static %} -{% block svelte_styles %} - {# {{ block.super }} #} - -{% endblock %} - -
- -{{ props|json_script:props_name }} - diff --git a/django_svelte/templates/django_svelte/display_svelte.html b/django_svelte/templates/django_svelte/display_svelte.html new file mode 100644 index 0000000..8949afc --- /dev/null +++ b/django_svelte/templates/django_svelte/display_svelte.html @@ -0,0 +1,10 @@ +{% comment %} +Create a target element +load the compiled bundle +instantiate the bundle with the json context +{% endcomment %} + +
+ +{{ props|json_script:props_name }} + diff --git a/django_svelte/templates/django_svelte/display_svelte_css.html b/django_svelte/templates/django_svelte/display_svelte_css.html new file mode 100644 index 0000000..0d31e86 --- /dev/null +++ b/django_svelte/templates/django_svelte/display_svelte_css.html @@ -0,0 +1,3 @@ +{% if css_bundle_url %} + +{% endif %} \ No newline at end of file diff --git a/django_svelte/templatetags/django_svelte.py b/django_svelte/templatetags/django_svelte.py index 44e1d23..4111167 100644 --- a/django_svelte/templatetags/django_svelte.py +++ b/django_svelte/templatetags/django_svelte.py @@ -1,26 +1,76 @@ -from os import path from django import template +from django.conf import settings +from django.contrib.staticfiles import finders from django.contrib.staticfiles.storage import staticfiles_storage register = template.Library() -@register.inclusion_tag("display_svelte.html") -def display_svelte(component, component_props={"name": "world"}): - if not component.endswith(".svelte"): - raise ValueError("component name should end with '.svelte'") - app_name = component[:-7] +def get_hashed_filename(*, component_name, file_type): + key = f"{settings.DJANGO_SVELTE_ENTRYPOINT_PREFIX}{component_name}.js" + entry = settings.DJANGO_SVELTE__VITE_MANIFEST.get(key) + if entry is None: + return None + if not entry.get("isEntry", False): + return None + if file_type == "js": + filename = entry["file"] + if file_type == "css": + _css = entry.get("css", []) + if not _css: + return None + filename = _css[0] + # strip the vite build.assetsDir from the filename + if filename.startswith(settings.DJANGO_SVELTE_VITE_ASSETSDIR): + _len = len(settings.DJANGO_SVELTE_VITE_ASSETSDIR) + filename = filename[_len:] + return filename - context = { - "bundle_url": staticfiles_storage.url(f"{app_name}.js"), - "css_bundle_url": staticfiles_storage.url(f"{app_name}.css"), - "element_id": f"{app_name.lower()}-target", - "props_name": f"{app_name.lower()}-props", - "app_name": app_name, - "props": component_props, + +def get_static_file_url(*, component_name, file_type="js"): + filename = None + if settings.DJANGO_SVELTE__VITE_MANIFEST is not None: + filename = get_hashed_filename( + component_name=component_name, file_type=file_type + ) + else: + filename = f"{component_name}.{file_type}" + if filename is None: + return None + static_file = finders.find(filename) + if static_file is None: + return None + return staticfiles_storage.url(filename) + + +def de_svelte(name): + """Removes the .svelte suffix from a name, if present""" + if name.endswith(".svelte"): + return name[:-7] + return name + + +@register.inclusion_tag("django_svelte/display_svelte_css.html") +def display_svelte_css(component_name): + component_name = de_svelte(component_name) + + return { + "css_bundle_url": get_static_file_url( + component_name=component_name, file_type="css" + ), } - if not path.exists(context["css_bundle_url"]): - context.pop("css_bundle_url") - return context +@register.inclusion_tag("django_svelte/display_svelte.html") +def display_svelte(component_name, component_props=None): + component_name = de_svelte(component_name) + component_props = component_props or {} + + return { + "bundle_url": get_static_file_url( + component_name=component_name, file_type="js" + ), + "element_id": f"{component_name.lower()}-target", + "props_name": f"{component_name.lower()}-props", + "props": component_props, + } diff --git a/django_svelte/views.py b/django_svelte/views.py new file mode 100644 index 0000000..733d8ec --- /dev/null +++ b/django_svelte/views.py @@ -0,0 +1,30 @@ +from django.views.generic import TemplateView +from django.views.generic.base import ContextMixin + + +class SvelteContextMixin(ContextMixin): + component_name = None + page_title = "Svelte Component" + + def get_svelte_props(self, **kwargs): + raise NotImplementedError( + "Define `get_svelte_props` in BaseSvelteComponentView subclass." + ) + + def get_context_data(self, *, svelte_props=None, **kwargs): + context = super().get_context_data(**kwargs) + _svelte_props = svelte_props or {} + context.update( + { + "page_title": self.page_title, + "component_name": self.component_name, + "component_props": self.get_svelte_props(**_svelte_props), + } + ) + return context + + +class SvelteTemplateView(SvelteContextMixin, TemplateView): + """Display Svelte component. (Provide your own conformant template)""" + + pass diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b291425 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +exclude = 'migrations/' + +[tool.isort] +profile = "black" +extend_skip_glob = ["*/migrations"] + +[tool.flake8] +exclude = ["migrations"] +ignore = ["E402", "W503"] +max-line-length = 100 +max-complexity = 10 + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["demo_project/django_svelte_demo"] diff --git a/setup.py b/setup.py index 39882ee..d135b25 100644 --- a/setup.py +++ b/setup.py @@ -7,22 +7,33 @@ name="django-svelte", author="Paul Stiverson", url="https://github.com/thismatters/django-svelte/", - version="0.1.6", - packages=find_packages(), + version="0.2.0", + packages=find_packages(exclude=("tests", "demo_project")), license="MIT", description="Facilitates adding Svelte frontend to Django", long_description=long_description, long_description_content_type="text/markdown", - python_requires=">=3.7.4", + python_requires=">=3.8", include_package_data=True, + install_requires=[ + "Django>=3.2.0", + "django-appconf>=1.0.0", + ], classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.7", - "Framework :: Django :: 3.1", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_conf.py b/tests/test_conf.py new file mode 100644 index 0000000..c701c12 --- /dev/null +++ b/tests/test_conf.py @@ -0,0 +1,52 @@ +from pathlib import Path + +import pytest +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +from django_svelte.conf import DjangoSvelteAppConf + + +class TestDjangoSvelteAppConf: + def test_configure__vite_manifest(self): + assert DjangoSvelteAppConf().configure__vite_manifest(None) is None + + def test_configure__vite_manifest_bad(self): + with pytest.raises(ImproperlyConfigured): + DjangoSvelteAppConf().configure__vite_manifest("") + + def test_configure_entrypoint_prefix(self): + assert DjangoSvelteAppConf().configure_entrypoint_prefix("slash/") == "slash/" + + def test_configure_entrypoint_prefix_none(self): + assert DjangoSvelteAppConf().configure_entrypoint_prefix(None) == "" + + def test_configure_entrypoint_prefix_bad(self): + with pytest.raises(ImproperlyConfigured): + DjangoSvelteAppConf().configure_entrypoint_prefix("noslash") + + def test_configure_vite_assetsdir(self): + assert DjangoSvelteAppConf().configure_vite_assetsdir("slash/") == "slash/" + + def test_configure_vite_assetsdir_none(self): + assert DjangoSvelteAppConf().configure_vite_assetsdir(None) == "" + + def test_configure_vite_assetsdir_bad(self): + with pytest.raises(ImproperlyConfigured): + DjangoSvelteAppConf().configure_vite_assetsdir("noslash") + + def test_configure(self): + conf = DjangoSvelteAppConf() + conf._meta.configured_data = { + "VITE_MANIFEST_PATH": Path( + settings.BASE_DIR.parent / "static" / "fake_manifest.json" + ) + } + ret = conf.configure() + assert ret["_VITE_MANIFEST"] == {"testkey": "testvalue"} + + def test_configure_no_file(self): + conf = DjangoSvelteAppConf() + conf._meta.configured_data = {"VITE_MANIFEST_PATH": Path("bad/path")} + with pytest.raises(ImproperlyConfigured): + ret = conf.configure() diff --git a/tests/test_django_svelte.py b/tests/test_django_svelte.py new file mode 100644 index 0000000..e226952 --- /dev/null +++ b/tests/test_django_svelte.py @@ -0,0 +1,132 @@ +from django.test import override_settings + +from django_svelte.templatetags.django_svelte import ( + get_hashed_filename, + get_static_file_url, + de_svelte, + display_svelte_css, + display_svelte, +) + + +class TestGetStaticFileUrl: + def test_none(self): + assert ( + get_static_file_url(component_name="not-a_file", file_type="asdf") is None + ) + + def test_real(self): + assert ( + get_static_file_url(component_name="real_file", file_type="txt") + == "/static/real_file.txt" + ) + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/Hashed.js": {"isEntry": True, "file": "assets/Hashed-asdfsafd.js"} + } + ) + def test_hashed(self): + assert ( + get_static_file_url(component_name="Hashed", file_type="js") + == "/static/Hashed-asdfsafd.js" + ) + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/Bashed.js": {"isEntry": True, "file": "assets/Bashed-asdfsafd.js"} + } + ) + def test_hashed_missing(self): + assert get_static_file_url(component_name="Bashed", file_type="js") is None + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/Bashed.js": {"isEntry": True, "file": "assets/Bashed-asdfsafd.js"} + } + ) + def test_hashed_absent(self): + assert get_static_file_url(component_name="Hashed", file_type="js") is None + + +def test_de_svelte(): + assert de_svelte("something.svelte") == "something" + + +def test_de_svelte_no_change(): + assert de_svelte("something.smelte") == "something.smelte" + + +def test_display_svelte_css(): + assert display_svelte_css("Something.svelte") == { + "css_bundle_url": "/static/Something.css" + } + + +def test_display_svelte(): + assert display_svelte("Something.svelte", component_props={"great": "stuff"}) == { + "bundle_url": "/static/Something.js", + "element_id": "something-target", + "props_name": "something-props", + "props": {"great": "stuff"}, + } + + +class TestGetHashedFilename: + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/App.js": {"isEntry": True, "file": "assets/App-asdfsafd.js"} + } + ) + def test_good(self): + assert ( + get_hashed_filename(component_name="App", file_type="js") + == "App-asdfsafd.js" + ) + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/App.js": {"isEntry": True, "css": ["assets/App-asdfsafd.css"]} + } + ) + def test_css(self): + assert ( + get_hashed_filename(component_name="App", file_type="css") + == "App-asdfsafd.css" + ) + + @override_settings(DJANGO_SVELTE__VITE_MANIFEST={"src/App.js": {"isEntry": True}}) + def test_css_none(self): + assert get_hashed_filename(component_name="App", file_type="css") is None + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/App.js": {"isEntry": True, "file": "assets/App-asdfsafd.js"} + } + ) + def test_nonexistent(self): + assert get_hashed_filename(component_name="Abb", file_type="js") is None + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={"src/App.js": {"file": "assets/App-asdfsafd.js"}} + ) + def test_nonentry(self): + assert get_hashed_filename(component_name="App", file_type="js") is None + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/App.js": {"file": "bassets/App-asdfsafd.js"} + }, + DJANGO_SVELTE_VITE_ASSETSDIR="bassets/", + ) + def test_different_assetsdir(self): + assert get_hashed_filename(component_name="App", file_type="js") is None + + @override_settings( + DJANGO_SVELTE__VITE_MANIFEST={ + "src/entrypoints/App.js": {"file": "assets/App-asdfsafd.js"} + }, + DJANGO_SVELTE_ENTRYPOINT_PREFIX="src/entrypoints/", + ) + def test_different_prefix(self): + assert get_hashed_filename(component_name="App", file_type="js") is None diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 0000000..1d89643 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,34 @@ +from unittest.mock import patch + +import pytest + +from django_svelte import views + + +def mocked_get_svelte_props(self, **kwargs): + kwargs.update( + { + "new_prop": "here", + } + ) + return kwargs + + +class TestSvelteComponentMixin: + @patch.object(views.SvelteContextMixin, "get_svelte_props", mocked_get_svelte_props) + def test_get_context_data(self): + mixin = views.SvelteContextMixin() + mixin.component_name = "FakeComponent" + mixin.page_title = "Fake Component" + ctx = mixin.get_context_data() + assert "page_title" in ctx + assert ctx["page_title"] == "Fake Component" + assert "component_name" in ctx + assert ctx["component_name"] == "FakeComponent" + assert "component_props" in ctx + assert ctx["component_props"] == {"new_prop": "here"} + + def test_get_context_data_no_subclass(self): + mixin = views.SvelteContextMixin() + with pytest.raises(NotImplementedError): + ctx = mixin.get_context_data() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e99b525 --- /dev/null +++ b/tox.ini @@ -0,0 +1,45 @@ +[tox] +requires = + tox>=4 +env_list = lint, py{38,39,310,311}-django{32,41,42}, py{310,311}-django{50} + + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + + +[testenv] +description = run unit tests +setenv = + DJANGO_SETTINGS_MODULE=django_svelte_demo.settings.test +deps = + pytest>=7 + pytest-django + pytest-cov + djangorestframework==3.14.0 + drf-yasg==1.21.7 + django32: Django>=3.2,<3.3 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 + django50: Django>=5.0,<5.1 +commands = + pytest --cov --cov-report xml {posargs:tests} +usedevelop = True + +[testenv:lint] +description = run linters +skip_install = true +deps = + black==24.1.1 + isort==5.13.2 + pyproject-flake8==6.1.0 +changedir = django_svelte +commands = + black --check {posargs:.} + isort --check --diff {posargs:.} + pflake8