django-vite-rolling 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_vite_rolling-0.1.0/.github/workflows/release.yml +37 -0
- django_vite_rolling-0.1.0/.github/workflows/test.yml +29 -0
- django_vite_rolling-0.1.0/.gitignore +32 -0
- django_vite_rolling-0.1.0/LICENSE +21 -0
- django_vite_rolling-0.1.0/PKG-INFO +132 -0
- django_vite_rolling-0.1.0/README.md +97 -0
- django_vite_rolling-0.1.0/pyproject.toml +62 -0
- django_vite_rolling-0.1.0/setup.cfg +4 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/__init__.py +1 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/apps.py +6 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/conf.py +30 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/management/__init__.py +0 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/management/commands/__init__.py +0 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/management/commands/refresh_vite_manifest.py +57 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/manifest.py +32 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/templatetags/__init__.py +0 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling/templatetags/vite.py +75 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling.egg-info/PKG-INFO +132 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling.egg-info/SOURCES.txt +27 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling.egg-info/dependency_links.txt +1 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling.egg-info/requires.txt +7 -0
- django_vite_rolling-0.1.0/src/django_vite_rolling.egg-info/top_level.txt +1 -0
- django_vite_rolling-0.1.0/tests/__init__.py +0 -0
- django_vite_rolling-0.1.0/tests/conftest.py +31 -0
- django_vite_rolling-0.1.0/tests/settings.py +43 -0
- django_vite_rolling-0.1.0/tests/test_conf.py +37 -0
- django_vite_rolling-0.1.0/tests/test_manifest.py +35 -0
- django_vite_rolling-0.1.0/tests/test_refresh_command.py +67 -0
- django_vite_rolling-0.1.0/tests/test_templatetags.py +55 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
with:
|
|
13
|
+
fetch-depth: 0 # setuptools-scm needs full history for version
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- name: Build
|
|
18
|
+
run: |
|
|
19
|
+
python -m pip install -U pip build
|
|
20
|
+
python -m build
|
|
21
|
+
- uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment: pypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write # required for PyPI trusted publishing
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python: ["3.11", "3.12", "3.13"]
|
|
15
|
+
django: ["4.2", "5.0", "5.1"]
|
|
16
|
+
exclude:
|
|
17
|
+
- python: "3.13"
|
|
18
|
+
django: "4.2"
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python }}
|
|
24
|
+
- name: Install
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install -U pip
|
|
27
|
+
python -m pip install "Django~=${{ matrix.django }}.0" -e ".[test]"
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: pytest -v
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
.eggs/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
.tox/
|
|
11
|
+
.nox/
|
|
12
|
+
.coverage
|
|
13
|
+
.coverage.*
|
|
14
|
+
htmlcov/
|
|
15
|
+
coverage.xml
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
|
|
20
|
+
# Virtualenvs
|
|
21
|
+
.venv/
|
|
22
|
+
venv/
|
|
23
|
+
env/
|
|
24
|
+
|
|
25
|
+
# Editors
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wedgworth's, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-vite-rolling
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lean Django + Vite integration with rolling-deploy-safe manifest caching.
|
|
5
|
+
Author-email: Patrick Altman <patrick@wedgworth.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wedgworth/django-vite-rolling
|
|
8
|
+
Project-URL: Issues, https://github.com/wedgworth/django-vite-rolling/issues
|
|
9
|
+
Project-URL: Source, https://github.com/wedgworth/django-vite-rolling
|
|
10
|
+
Keywords: django,vite,manifest,rolling-deploy
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 4.2
|
|
15
|
+
Classifier: Framework :: Django :: 5.0
|
|
16
|
+
Classifier: Framework :: Django :: 5.1
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: django>=4.2
|
|
29
|
+
Requires-Dist: django-redis>=5.0
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-django>=4; extra == "test"
|
|
33
|
+
Requires-Dist: fakeredis>=2; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# django-vite-rolling
|
|
37
|
+
|
|
38
|
+
A lean Django + Vite integration with rolling-deploy-safe manifest caching.
|
|
39
|
+
|
|
40
|
+
Roughly 100 lines of code. No React Refresh, no polyfills, no legacy bundle handling — just `{% vite_scripts %}`, `{% vite_styles %}`, and a management command that keeps Vite manifest caches consistent across rolling deploys.
|
|
41
|
+
|
|
42
|
+
## Why this exists
|
|
43
|
+
|
|
44
|
+
Most Django + Vite integrations cache one manifest globally. On a rolling deploy, both the old and new app versions can serve requests simultaneously, each needing their own manifest. This package caches manifests under versioned Redis keys (`vite_manifest:<RELEASE_VERSION>`) and provides a management command to prune stale versions during deploys.
|
|
45
|
+
|
|
46
|
+
If you don't need versioned/rolling-deploy support, you probably want [`django-vite`](https://github.com/MrBin99/django-vite) instead.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install django-vite-rolling
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add to `INSTALLED_APPS`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
INSTALLED_APPS = [
|
|
58
|
+
# ...
|
|
59
|
+
"django_vite_rolling",
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires `django-redis` configured as your default cache backend.
|
|
64
|
+
|
|
65
|
+
## Configure
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
VITE = {
|
|
69
|
+
"manifest_path": BASE_DIR / "static" / ".vite" / "manifest.json",
|
|
70
|
+
"cache": not DEBUG,
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### All settings
|
|
75
|
+
|
|
76
|
+
| Key | Default | Description |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `manifest_path` | *(required)* | Path to Vite's `manifest.json` |
|
|
79
|
+
| `cache` | `True` | Whether to cache the loaded manifest in Redis (set `False` in dev) |
|
|
80
|
+
| `cache_key_prefix` | `"vite_manifest"` | Redis key prefix; full key is `<prefix>:<RELEASE_VERSION>` |
|
|
81
|
+
| `dev_server_host` | `None` | Dev server host. `None` → derive from request `Host` header (fallback `localhost`) |
|
|
82
|
+
| `dev_server_port` | `3001` | Dev server port |
|
|
83
|
+
| `dev_server_static_path` | `"/static"` | Path prefix Vite serves from |
|
|
84
|
+
| `versions_to_keep` | `5` | How many recent release versions to retain manifests for |
|
|
85
|
+
| `version_setting` | `"RELEASE_VERSION"` | Name of the Django setting holding the current release identifier |
|
|
86
|
+
| `versions_redis_key` | `"recent-manifest-versions"` | Redis list key tracking recent versions |
|
|
87
|
+
| `redis_alias` | `"default"` | django-redis alias to use |
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
In your base template:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
{% load vite %}
|
|
95
|
+
<!DOCTYPE html>
|
|
96
|
+
<html>
|
|
97
|
+
<head>
|
|
98
|
+
{% vite_styles "src/main.ts" %}
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
{% vite_scripts "src/main.ts" %}
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In `DEBUG` mode the tags inject Vite's HMR client and module URLs pointing at the dev server. In production they resolve the named entries through the manifest, including recursively-imported chunks and their CSS.
|
|
107
|
+
|
|
108
|
+
## Post-deploy
|
|
109
|
+
|
|
110
|
+
Run after each deploy (e.g., in a release-phase / post-deploy hook):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python manage.py refresh_vite_manifest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This:
|
|
117
|
+
1. Records `RELEASE_VERSION` in a Redis list of recent versions (truncated to `versions_to_keep`).
|
|
118
|
+
2. Scans for `vite_manifest:*` keys whose suffix is not in the recent list and deletes them.
|
|
119
|
+
3. Loads the current manifest and caches it under `vite_manifest:<RELEASE_VERSION>`.
|
|
120
|
+
|
|
121
|
+
If `RELEASE_VERSION` is empty, the command caches the manifest under the bare prefix and skips cleanup.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[test]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# django-vite-rolling
|
|
2
|
+
|
|
3
|
+
A lean Django + Vite integration with rolling-deploy-safe manifest caching.
|
|
4
|
+
|
|
5
|
+
Roughly 100 lines of code. No React Refresh, no polyfills, no legacy bundle handling — just `{% vite_scripts %}`, `{% vite_styles %}`, and a management command that keeps Vite manifest caches consistent across rolling deploys.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Most Django + Vite integrations cache one manifest globally. On a rolling deploy, both the old and new app versions can serve requests simultaneously, each needing their own manifest. This package caches manifests under versioned Redis keys (`vite_manifest:<RELEASE_VERSION>`) and provides a management command to prune stale versions during deploys.
|
|
10
|
+
|
|
11
|
+
If you don't need versioned/rolling-deploy support, you probably want [`django-vite`](https://github.com/MrBin99/django-vite) instead.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install django-vite-rolling
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Add to `INSTALLED_APPS`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
INSTALLED_APPS = [
|
|
23
|
+
# ...
|
|
24
|
+
"django_vite_rolling",
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires `django-redis` configured as your default cache backend.
|
|
29
|
+
|
|
30
|
+
## Configure
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
VITE = {
|
|
34
|
+
"manifest_path": BASE_DIR / "static" / ".vite" / "manifest.json",
|
|
35
|
+
"cache": not DEBUG,
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### All settings
|
|
40
|
+
|
|
41
|
+
| Key | Default | Description |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `manifest_path` | *(required)* | Path to Vite's `manifest.json` |
|
|
44
|
+
| `cache` | `True` | Whether to cache the loaded manifest in Redis (set `False` in dev) |
|
|
45
|
+
| `cache_key_prefix` | `"vite_manifest"` | Redis key prefix; full key is `<prefix>:<RELEASE_VERSION>` |
|
|
46
|
+
| `dev_server_host` | `None` | Dev server host. `None` → derive from request `Host` header (fallback `localhost`) |
|
|
47
|
+
| `dev_server_port` | `3001` | Dev server port |
|
|
48
|
+
| `dev_server_static_path` | `"/static"` | Path prefix Vite serves from |
|
|
49
|
+
| `versions_to_keep` | `5` | How many recent release versions to retain manifests for |
|
|
50
|
+
| `version_setting` | `"RELEASE_VERSION"` | Name of the Django setting holding the current release identifier |
|
|
51
|
+
| `versions_redis_key` | `"recent-manifest-versions"` | Redis list key tracking recent versions |
|
|
52
|
+
| `redis_alias` | `"default"` | django-redis alias to use |
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
In your base template:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
{% load vite %}
|
|
60
|
+
<!DOCTYPE html>
|
|
61
|
+
<html>
|
|
62
|
+
<head>
|
|
63
|
+
{% vite_styles "src/main.ts" %}
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
{% vite_scripts "src/main.ts" %}
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In `DEBUG` mode the tags inject Vite's HMR client and module URLs pointing at the dev server. In production they resolve the named entries through the manifest, including recursively-imported chunks and their CSS.
|
|
72
|
+
|
|
73
|
+
## Post-deploy
|
|
74
|
+
|
|
75
|
+
Run after each deploy (e.g., in a release-phase / post-deploy hook):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python manage.py refresh_vite_manifest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This:
|
|
82
|
+
1. Records `RELEASE_VERSION` in a Redis list of recent versions (truncated to `versions_to_keep`).
|
|
83
|
+
2. Scans for `vite_manifest:*` keys whose suffix is not in the recent list and deletes them.
|
|
84
|
+
3. Loads the current manifest and caches it under `vite_manifest:<RELEASE_VERSION>`.
|
|
85
|
+
|
|
86
|
+
If `RELEASE_VERSION` is empty, the command caches the manifest under the bare prefix and skips cleanup.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install -e ".[test]"
|
|
92
|
+
pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "setuptools-scm>=8"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-vite-rolling"
|
|
7
|
+
description = "Lean Django + Vite integration with rolling-deploy-safe manifest caching."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
authors = [{ name = "Patrick Altman", email = "patrick@wedgworth.com" }]
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["django", "vite", "manifest", "rolling-deploy"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Web Environment",
|
|
16
|
+
"Framework :: Django",
|
|
17
|
+
"Framework :: Django :: 4.2",
|
|
18
|
+
"Framework :: Django :: 5.0",
|
|
19
|
+
"Framework :: Django :: 5.1",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"django>=4.2",
|
|
31
|
+
"django-redis>=5.0",
|
|
32
|
+
]
|
|
33
|
+
dynamic = ["version"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/wedgworth/django-vite-rolling"
|
|
37
|
+
Issues = "https://github.com/wedgworth/django-vite-rolling/issues"
|
|
38
|
+
Source = "https://github.com/wedgworth/django-vite-rolling"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
test = [
|
|
42
|
+
"pytest>=7",
|
|
43
|
+
"pytest-django>=4",
|
|
44
|
+
"fakeredis>=2",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools_scm]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
54
|
+
python_files = ["test_*.py"]
|
|
55
|
+
pythonpath = ["."]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 120
|
|
59
|
+
target-version = "py311"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "W", "UP"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default_app_config = "django_vite_rolling.apps.DjangoViteRollingConfig"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DEFAULTS = {
|
|
5
|
+
"manifest_path": None,
|
|
6
|
+
"cache": True,
|
|
7
|
+
"cache_key_prefix": "vite_manifest",
|
|
8
|
+
"dev_server_host": None,
|
|
9
|
+
"dev_server_port": 3001,
|
|
10
|
+
"dev_server_static_path": "/static",
|
|
11
|
+
"versions_to_keep": 5,
|
|
12
|
+
"version_setting": "RELEASE_VERSION",
|
|
13
|
+
"versions_redis_key": "recent-manifest-versions",
|
|
14
|
+
"redis_alias": "default",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_setting(key: str):
|
|
19
|
+
user_settings = getattr(settings, "VITE", {})
|
|
20
|
+
return user_settings.get(key, DEFAULTS[key])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_release_version() -> str:
|
|
24
|
+
return getattr(settings, get_setting("version_setting"), "") or ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_cache_key() -> str:
|
|
28
|
+
prefix = get_setting("cache_key_prefix")
|
|
29
|
+
version = get_release_version()
|
|
30
|
+
return f"{prefix}:{version}" if version else prefix
|
|
File without changes
|
|
File without changes
|
django_vite_rolling-0.1.0/src/django_vite_rolling/management/commands/refresh_vite_manifest.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from django.core.management import BaseCommand
|
|
2
|
+
from django_redis import get_redis_connection
|
|
3
|
+
from redis.exceptions import RedisError
|
|
4
|
+
|
|
5
|
+
from django_vite_rolling.conf import get_release_version, get_setting
|
|
6
|
+
from django_vite_rolling.manifest import set_manifest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Command(BaseCommand):
|
|
10
|
+
help = "Refresh the Vite manifest cache and prune stale versions for rolling deploys."
|
|
11
|
+
|
|
12
|
+
def handle(self, *args, **options):
|
|
13
|
+
version = get_release_version()
|
|
14
|
+
|
|
15
|
+
if not version:
|
|
16
|
+
self.stdout.write(self.style.WARNING(
|
|
17
|
+
f"{get_setting('version_setting')} is empty; caching manifest without versioned cleanup."
|
|
18
|
+
))
|
|
19
|
+
set_manifest()
|
|
20
|
+
self.stdout.write(self.style.SUCCESS("Vite manifest cached."))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
versions_to_keep = get_setting("versions_to_keep")
|
|
24
|
+
prefix_match = f"{get_setting('cache_key_prefix')}:*"
|
|
25
|
+
versions_key = get_setting("versions_redis_key")
|
|
26
|
+
redis_alias = get_setting("redis_alias")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
client = get_redis_connection(redis_alias)
|
|
30
|
+
client.lpush(versions_key, version.encode("utf-8"))
|
|
31
|
+
if versions_to_keep:
|
|
32
|
+
client.ltrim(versions_key, 0, versions_to_keep - 1)
|
|
33
|
+
|
|
34
|
+
recent = {v.decode("utf-8") for v in client.lrange(versions_key, 0, -1)}
|
|
35
|
+
self.stdout.write(f"Recent versions: {sorted(recent)}")
|
|
36
|
+
|
|
37
|
+
deleted = 0
|
|
38
|
+
cursor = 0
|
|
39
|
+
while True:
|
|
40
|
+
cursor, keys = client.scan(cursor=cursor, match=prefix_match, count=100)
|
|
41
|
+
for key in keys:
|
|
42
|
+
key_str = key.decode("utf-8")
|
|
43
|
+
if not any(key_str.endswith(f":{v}") for v in recent):
|
|
44
|
+
client.delete(key)
|
|
45
|
+
deleted += 1
|
|
46
|
+
self.stdout.write(f"Deleted stale manifest key: {key_str}")
|
|
47
|
+
if cursor == 0:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
self.stdout.write(self.style.SUCCESS(
|
|
51
|
+
f"Added '{version}' to recent versions; deleted {deleted} stale key(s)."
|
|
52
|
+
))
|
|
53
|
+
|
|
54
|
+
set_manifest()
|
|
55
|
+
self.stdout.write(self.style.SUCCESS("Vite manifest cached."))
|
|
56
|
+
except RedisError as e:
|
|
57
|
+
self.stdout.write(self.style.ERROR(f"Redis error: {e}"))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from django.core.cache import cache
|
|
5
|
+
|
|
6
|
+
from django_vite_rolling.conf import get_cache_key, get_setting
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING: # pragma: no cover
|
|
10
|
+
ChunkType = typing.TypedDict(
|
|
11
|
+
"ChunkType",
|
|
12
|
+
{"file": str, "css": list[str], "imports": list[str]},
|
|
13
|
+
total=False,
|
|
14
|
+
)
|
|
15
|
+
ManifestType = typing.Mapping[str, ChunkType]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_manifest() -> "ManifestType":
|
|
19
|
+
manifest_path = get_setting("manifest_path")
|
|
20
|
+
if not manifest_path:
|
|
21
|
+
raise RuntimeError("VITE['manifest_path'] is not configured")
|
|
22
|
+
with open(manifest_path) as fp:
|
|
23
|
+
manifest: ManifestType = json.load(fp)
|
|
24
|
+
cache.set(get_cache_key(), manifest, None)
|
|
25
|
+
return manifest
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_manifest() -> "ManifestType":
|
|
29
|
+
if cached := cache.get(get_cache_key()):
|
|
30
|
+
if get_setting("cache"):
|
|
31
|
+
return cached
|
|
32
|
+
return set_manifest()
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from django import template
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.templatetags.static import static
|
|
7
|
+
from django.utils.safestring import mark_safe
|
|
8
|
+
|
|
9
|
+
from django_vite_rolling.conf import get_setting
|
|
10
|
+
from django_vite_rolling.manifest import get_manifest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING: # pragma: no cover
|
|
14
|
+
from django.utils.safestring import SafeString
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
register = template.Library()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_absolute_url(url: str) -> bool:
|
|
21
|
+
return re.match("^https?://", url) is not None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _dev_server_root(request=None) -> str:
|
|
25
|
+
host = get_setting("dev_server_host")
|
|
26
|
+
if host is None:
|
|
27
|
+
host = "localhost"
|
|
28
|
+
if request is not None:
|
|
29
|
+
host = request.get_host().split(":")[0]
|
|
30
|
+
port = get_setting("dev_server_port")
|
|
31
|
+
static_path = get_setting("dev_server_static_path")
|
|
32
|
+
return f"http://{host}:{port}{static_path}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def vite_manifest(entries_names: typing.Sequence[str], request=None) -> tuple[list[str], list[str]]:
|
|
36
|
+
if settings.DEBUG:
|
|
37
|
+
dev_root = _dev_server_root(request)
|
|
38
|
+
scripts = [f"{dev_root}/@vite/client"] + [f"{dev_root}/{name}" for name in entries_names]
|
|
39
|
+
return scripts, []
|
|
40
|
+
|
|
41
|
+
manifest = get_manifest()
|
|
42
|
+
seen: set[str] = set()
|
|
43
|
+
|
|
44
|
+
def _process(names: typing.Sequence[str]) -> tuple[list[str], list[str]]:
|
|
45
|
+
scripts: list[str] = []
|
|
46
|
+
styles: list[str] = []
|
|
47
|
+
for name in names:
|
|
48
|
+
if name in seen:
|
|
49
|
+
continue
|
|
50
|
+
chunk = manifest[name]
|
|
51
|
+
import_scripts, import_styles = _process(chunk.get("imports", []))
|
|
52
|
+
scripts.extend(import_scripts)
|
|
53
|
+
styles.extend(import_styles)
|
|
54
|
+
scripts.append(chunk["file"])
|
|
55
|
+
styles.extend(chunk.get("css", []))
|
|
56
|
+
seen.add(name)
|
|
57
|
+
return scripts, styles
|
|
58
|
+
|
|
59
|
+
return _process(entries_names)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@register.simple_tag(name="vite_styles", takes_context=True)
|
|
63
|
+
def vite_styles(context, *entries_names: str) -> "SafeString":
|
|
64
|
+
request = context.get("request")
|
|
65
|
+
_, styles = vite_manifest(entries_names, request=request)
|
|
66
|
+
hrefs = (href if _is_absolute_url(href) else static(href) for href in styles)
|
|
67
|
+
return mark_safe("\n".join(f'<link rel="stylesheet" href="{href}" />' for href in hrefs)) # nosec
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@register.simple_tag(name="vite_scripts", takes_context=True)
|
|
71
|
+
def vite_scripts(context, *entries_names: str) -> "SafeString":
|
|
72
|
+
request = context.get("request")
|
|
73
|
+
scripts, _ = vite_manifest(entries_names, request=request)
|
|
74
|
+
srcs = (src if _is_absolute_url(src) else static(src) for src in scripts)
|
|
75
|
+
return mark_safe("\n".join(f'<script type="module" src="{src}"></script>' for src in srcs)) # nosec
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-vite-rolling
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lean Django + Vite integration with rolling-deploy-safe manifest caching.
|
|
5
|
+
Author-email: Patrick Altman <patrick@wedgworth.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wedgworth/django-vite-rolling
|
|
8
|
+
Project-URL: Issues, https://github.com/wedgworth/django-vite-rolling/issues
|
|
9
|
+
Project-URL: Source, https://github.com/wedgworth/django-vite-rolling
|
|
10
|
+
Keywords: django,vite,manifest,rolling-deploy
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 4.2
|
|
15
|
+
Classifier: Framework :: Django :: 5.0
|
|
16
|
+
Classifier: Framework :: Django :: 5.1
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: django>=4.2
|
|
29
|
+
Requires-Dist: django-redis>=5.0
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-django>=4; extra == "test"
|
|
33
|
+
Requires-Dist: fakeredis>=2; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# django-vite-rolling
|
|
37
|
+
|
|
38
|
+
A lean Django + Vite integration with rolling-deploy-safe manifest caching.
|
|
39
|
+
|
|
40
|
+
Roughly 100 lines of code. No React Refresh, no polyfills, no legacy bundle handling — just `{% vite_scripts %}`, `{% vite_styles %}`, and a management command that keeps Vite manifest caches consistent across rolling deploys.
|
|
41
|
+
|
|
42
|
+
## Why this exists
|
|
43
|
+
|
|
44
|
+
Most Django + Vite integrations cache one manifest globally. On a rolling deploy, both the old and new app versions can serve requests simultaneously, each needing their own manifest. This package caches manifests under versioned Redis keys (`vite_manifest:<RELEASE_VERSION>`) and provides a management command to prune stale versions during deploys.
|
|
45
|
+
|
|
46
|
+
If you don't need versioned/rolling-deploy support, you probably want [`django-vite`](https://github.com/MrBin99/django-vite) instead.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install django-vite-rolling
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add to `INSTALLED_APPS`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
INSTALLED_APPS = [
|
|
58
|
+
# ...
|
|
59
|
+
"django_vite_rolling",
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires `django-redis` configured as your default cache backend.
|
|
64
|
+
|
|
65
|
+
## Configure
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
VITE = {
|
|
69
|
+
"manifest_path": BASE_DIR / "static" / ".vite" / "manifest.json",
|
|
70
|
+
"cache": not DEBUG,
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### All settings
|
|
75
|
+
|
|
76
|
+
| Key | Default | Description |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `manifest_path` | *(required)* | Path to Vite's `manifest.json` |
|
|
79
|
+
| `cache` | `True` | Whether to cache the loaded manifest in Redis (set `False` in dev) |
|
|
80
|
+
| `cache_key_prefix` | `"vite_manifest"` | Redis key prefix; full key is `<prefix>:<RELEASE_VERSION>` |
|
|
81
|
+
| `dev_server_host` | `None` | Dev server host. `None` → derive from request `Host` header (fallback `localhost`) |
|
|
82
|
+
| `dev_server_port` | `3001` | Dev server port |
|
|
83
|
+
| `dev_server_static_path` | `"/static"` | Path prefix Vite serves from |
|
|
84
|
+
| `versions_to_keep` | `5` | How many recent release versions to retain manifests for |
|
|
85
|
+
| `version_setting` | `"RELEASE_VERSION"` | Name of the Django setting holding the current release identifier |
|
|
86
|
+
| `versions_redis_key` | `"recent-manifest-versions"` | Redis list key tracking recent versions |
|
|
87
|
+
| `redis_alias` | `"default"` | django-redis alias to use |
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
In your base template:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
{% load vite %}
|
|
95
|
+
<!DOCTYPE html>
|
|
96
|
+
<html>
|
|
97
|
+
<head>
|
|
98
|
+
{% vite_styles "src/main.ts" %}
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
{% vite_scripts "src/main.ts" %}
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In `DEBUG` mode the tags inject Vite's HMR client and module URLs pointing at the dev server. In production they resolve the named entries through the manifest, including recursively-imported chunks and their CSS.
|
|
107
|
+
|
|
108
|
+
## Post-deploy
|
|
109
|
+
|
|
110
|
+
Run after each deploy (e.g., in a release-phase / post-deploy hook):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python manage.py refresh_vite_manifest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This:
|
|
117
|
+
1. Records `RELEASE_VERSION` in a Redis list of recent versions (truncated to `versions_to_keep`).
|
|
118
|
+
2. Scans for `vite_manifest:*` keys whose suffix is not in the recent list and deletes them.
|
|
119
|
+
3. Loads the current manifest and caches it under `vite_manifest:<RELEASE_VERSION>`.
|
|
120
|
+
|
|
121
|
+
If `RELEASE_VERSION` is empty, the command caches the manifest under the bare prefix and skips cleanup.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[test]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
LICENSE
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
.github/workflows/release.yml
|
|
6
|
+
.github/workflows/test.yml
|
|
7
|
+
src/django_vite_rolling/__init__.py
|
|
8
|
+
src/django_vite_rolling/apps.py
|
|
9
|
+
src/django_vite_rolling/conf.py
|
|
10
|
+
src/django_vite_rolling/manifest.py
|
|
11
|
+
src/django_vite_rolling.egg-info/PKG-INFO
|
|
12
|
+
src/django_vite_rolling.egg-info/SOURCES.txt
|
|
13
|
+
src/django_vite_rolling.egg-info/dependency_links.txt
|
|
14
|
+
src/django_vite_rolling.egg-info/requires.txt
|
|
15
|
+
src/django_vite_rolling.egg-info/top_level.txt
|
|
16
|
+
src/django_vite_rolling/management/__init__.py
|
|
17
|
+
src/django_vite_rolling/management/commands/__init__.py
|
|
18
|
+
src/django_vite_rolling/management/commands/refresh_vite_manifest.py
|
|
19
|
+
src/django_vite_rolling/templatetags/__init__.py
|
|
20
|
+
src/django_vite_rolling/templatetags/vite.py
|
|
21
|
+
tests/__init__.py
|
|
22
|
+
tests/conftest.py
|
|
23
|
+
tests/settings.py
|
|
24
|
+
tests/test_conf.py
|
|
25
|
+
tests/test_manifest.py
|
|
26
|
+
tests/test_refresh_command.py
|
|
27
|
+
tests/test_templatetags.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_vite_rolling
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from django.core.cache import cache
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SAMPLE_MANIFEST = {
|
|
8
|
+
"src/main.ts": {
|
|
9
|
+
"file": "assets/main-abc123.js",
|
|
10
|
+
"css": ["assets/main-abc123.css"],
|
|
11
|
+
"imports": ["_shared.js"],
|
|
12
|
+
},
|
|
13
|
+
"_shared.js": {
|
|
14
|
+
"file": "assets/shared-def456.js",
|
|
15
|
+
"css": ["assets/shared-def456.css"],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def manifest_file(tmp_path):
|
|
22
|
+
path = tmp_path / "manifest.json"
|
|
23
|
+
path.write_text(json.dumps(SAMPLE_MANIFEST))
|
|
24
|
+
return path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(autouse=True)
|
|
28
|
+
def clear_cache():
|
|
29
|
+
cache.clear()
|
|
30
|
+
yield
|
|
31
|
+
cache.clear()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
SECRET_KEY = "test"
|
|
2
|
+
DEBUG = False
|
|
3
|
+
ALLOWED_HOSTS = ["*"]
|
|
4
|
+
|
|
5
|
+
INSTALLED_APPS = [
|
|
6
|
+
"django.contrib.contenttypes",
|
|
7
|
+
"django.contrib.auth",
|
|
8
|
+
"django.contrib.staticfiles",
|
|
9
|
+
"django_vite_rolling",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
DATABASES = {
|
|
13
|
+
"default": {
|
|
14
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
15
|
+
"NAME": ":memory:",
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
CACHES = {
|
|
20
|
+
"default": {
|
|
21
|
+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
TEMPLATES = [
|
|
26
|
+
{
|
|
27
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
28
|
+
"APP_DIRS": True,
|
|
29
|
+
"OPTIONS": {
|
|
30
|
+
"context_processors": [],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
STATIC_URL = "/static/"
|
|
36
|
+
USE_TZ = True
|
|
37
|
+
|
|
38
|
+
RELEASE_VERSION = "test-version"
|
|
39
|
+
|
|
40
|
+
VITE = {
|
|
41
|
+
"manifest_path": None,
|
|
42
|
+
"cache": True,
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from django.test import override_settings
|
|
2
|
+
|
|
3
|
+
from django_vite_rolling.conf import get_cache_key, get_release_version, get_setting
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_get_setting_returns_default_when_not_configured():
|
|
7
|
+
assert get_setting("cache_key_prefix") == "vite_manifest"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@override_settings(VITE={"cache_key_prefix": "custom_prefix"})
|
|
11
|
+
def test_get_setting_returns_user_override():
|
|
12
|
+
assert get_setting("cache_key_prefix") == "custom_prefix"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@override_settings(RELEASE_VERSION="abc123")
|
|
16
|
+
def test_get_release_version_reads_default_setting():
|
|
17
|
+
assert get_release_version() == "abc123"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@override_settings(RELEASE_VERSION="")
|
|
21
|
+
def test_get_release_version_blank_is_empty():
|
|
22
|
+
assert get_release_version() == ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@override_settings(RELEASE_VERSION="v1.2.3")
|
|
26
|
+
def test_get_cache_key_with_version():
|
|
27
|
+
assert get_cache_key() == "vite_manifest:v1.2.3"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@override_settings(RELEASE_VERSION="")
|
|
31
|
+
def test_get_cache_key_without_version_falls_back_to_prefix():
|
|
32
|
+
assert get_cache_key() == "vite_manifest"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@override_settings(MY_VERSION="x", VITE={"version_setting": "MY_VERSION"})
|
|
36
|
+
def test_version_setting_is_configurable():
|
|
37
|
+
assert get_release_version() == "x"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.core.cache import cache
|
|
3
|
+
from django.test import override_settings
|
|
4
|
+
|
|
5
|
+
from django_vite_rolling.manifest import get_manifest, set_manifest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_set_manifest_loads_from_file_and_caches(manifest_file):
|
|
9
|
+
with override_settings(VITE={"manifest_path": str(manifest_file)}, RELEASE_VERSION="v1"):
|
|
10
|
+
result = set_manifest()
|
|
11
|
+
assert "src/main.ts" in result
|
|
12
|
+
assert cache.get("vite_manifest:v1") == result
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_set_manifest_raises_when_path_unconfigured():
|
|
16
|
+
with override_settings(VITE={"manifest_path": None}):
|
|
17
|
+
with pytest.raises(RuntimeError, match="manifest_path"):
|
|
18
|
+
set_manifest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_manifest_returns_cached_when_cache_enabled(manifest_file):
|
|
22
|
+
with override_settings(VITE={"manifest_path": str(manifest_file), "cache": True}, RELEASE_VERSION="v1"):
|
|
23
|
+
first = set_manifest()
|
|
24
|
+
cache.set("vite_manifest:v1", {"sentinel": {"file": "x"}}, None)
|
|
25
|
+
second = get_manifest()
|
|
26
|
+
assert second == {"sentinel": {"file": "x"}}
|
|
27
|
+
assert second != first
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_get_manifest_re_reads_when_cache_disabled(manifest_file):
|
|
31
|
+
with override_settings(VITE={"manifest_path": str(manifest_file), "cache": False}, RELEASE_VERSION="v1"):
|
|
32
|
+
set_manifest()
|
|
33
|
+
cache.set("vite_manifest:v1", {"sentinel": {"file": "x"}}, None)
|
|
34
|
+
result = get_manifest()
|
|
35
|
+
assert "src/main.ts" in result
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import fakeredis
|
|
5
|
+
from django.core.management import call_command
|
|
6
|
+
from django.test import override_settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _fake_client():
|
|
10
|
+
return fakeredis.FakeRedis()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_command_warns_when_release_version_empty(manifest_file):
|
|
14
|
+
out = StringIO()
|
|
15
|
+
with override_settings(RELEASE_VERSION="", VITE={"manifest_path": str(manifest_file)}):
|
|
16
|
+
call_command("refresh_vite_manifest", stdout=out)
|
|
17
|
+
output = out.getvalue()
|
|
18
|
+
assert "RELEASE_VERSION is empty" in output
|
|
19
|
+
assert "Vite manifest cached" in output
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_command_records_version_and_caches_manifest(manifest_file):
|
|
23
|
+
client = _fake_client()
|
|
24
|
+
out = StringIO()
|
|
25
|
+
with override_settings(RELEASE_VERSION="v1", VITE={"manifest_path": str(manifest_file)}):
|
|
26
|
+
with patch("django_vite_rolling.management.commands.refresh_vite_manifest.get_redis_connection",
|
|
27
|
+
return_value=client):
|
|
28
|
+
call_command("refresh_vite_manifest", stdout=out)
|
|
29
|
+
versions = [v.decode("utf-8") for v in client.lrange("recent-manifest-versions", 0, -1)]
|
|
30
|
+
assert versions == ["v1"]
|
|
31
|
+
assert "Vite manifest cached" in out.getvalue()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_command_deletes_stale_versioned_keys(manifest_file):
|
|
35
|
+
client = _fake_client()
|
|
36
|
+
client.set("vite_manifest:old1", b"stale")
|
|
37
|
+
client.set("vite_manifest:old2", b"stale")
|
|
38
|
+
client.set("vite_manifest:keep", b"current")
|
|
39
|
+
client.lpush("recent-manifest-versions", b"keep")
|
|
40
|
+
|
|
41
|
+
out = StringIO()
|
|
42
|
+
with override_settings(RELEASE_VERSION="new", VITE={"manifest_path": str(manifest_file), "versions_to_keep": 2}):
|
|
43
|
+
with patch("django_vite_rolling.management.commands.refresh_vite_manifest.get_redis_connection",
|
|
44
|
+
return_value=client):
|
|
45
|
+
call_command("refresh_vite_manifest", stdout=out)
|
|
46
|
+
|
|
47
|
+
# "new" and "keep" should both be retained (versions_to_keep=2)
|
|
48
|
+
versions = {v.decode("utf-8") for v in client.lrange("recent-manifest-versions", 0, -1)}
|
|
49
|
+
assert versions == {"new", "keep"}
|
|
50
|
+
assert client.get("vite_manifest:old1") is None
|
|
51
|
+
assert client.get("vite_manifest:old2") is None
|
|
52
|
+
assert client.get("vite_manifest:keep") == b"current"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_command_trims_to_versions_to_keep(manifest_file):
|
|
56
|
+
client = _fake_client()
|
|
57
|
+
for v in ["a", "b", "c", "d", "e"]:
|
|
58
|
+
client.lpush("recent-manifest-versions", v.encode("utf-8"))
|
|
59
|
+
|
|
60
|
+
with override_settings(RELEASE_VERSION="f", VITE={"manifest_path": str(manifest_file), "versions_to_keep": 3}):
|
|
61
|
+
with patch("django_vite_rolling.management.commands.refresh_vite_manifest.get_redis_connection",
|
|
62
|
+
return_value=client):
|
|
63
|
+
call_command("refresh_vite_manifest", stdout=StringIO())
|
|
64
|
+
|
|
65
|
+
versions = [v.decode("utf-8") for v in client.lrange("recent-manifest-versions", 0, -1)]
|
|
66
|
+
assert len(versions) == 3
|
|
67
|
+
assert versions[0] == "f"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from django.template import Context, Template
|
|
2
|
+
from django.test import RequestFactory, override_settings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _render(template_source: str, request=None) -> str:
|
|
6
|
+
template = Template(template_source)
|
|
7
|
+
return template.render(Context({"request": request}))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@override_settings(DEBUG=True)
|
|
11
|
+
def test_dev_mode_emits_vite_client_and_entry():
|
|
12
|
+
result = _render('{% load vite %}{% vite_scripts "src/main.ts" %}')
|
|
13
|
+
assert "@vite/client" in result
|
|
14
|
+
assert "localhost:3001/static/src/main.ts" in result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@override_settings(DEBUG=True)
|
|
18
|
+
def test_dev_mode_uses_request_host():
|
|
19
|
+
rf = RequestFactory()
|
|
20
|
+
request = rf.get("/", HTTP_HOST="dev.box:8000")
|
|
21
|
+
result = _render('{% load vite %}{% vite_scripts "src/main.ts" %}', request=request)
|
|
22
|
+
assert "dev.box:3001" in result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@override_settings(DEBUG=True, VITE={"dev_server_host": "fixed.host", "dev_server_port": 5173})
|
|
26
|
+
def test_dev_mode_respects_configured_host_and_port():
|
|
27
|
+
result = _render('{% load vite %}{% vite_scripts "src/main.ts" %}')
|
|
28
|
+
assert "fixed.host:5173" in result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_prod_mode_resolves_entry_and_imports(manifest_file):
|
|
32
|
+
with override_settings(DEBUG=False, VITE={"manifest_path": str(manifest_file)}, RELEASE_VERSION="v1"):
|
|
33
|
+
scripts = _render('{% load vite %}{% vite_scripts "src/main.ts" %}')
|
|
34
|
+
# Imported chunk should come before the entry chunk
|
|
35
|
+
assert scripts.index("shared-def456.js") < scripts.index("main-abc123.js")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_prod_mode_emits_styles(manifest_file):
|
|
39
|
+
with override_settings(DEBUG=False, VITE={"manifest_path": str(manifest_file)}, RELEASE_VERSION="v1"):
|
|
40
|
+
styles = _render('{% load vite %}{% vite_styles "src/main.ts" %}')
|
|
41
|
+
assert "main-abc123.css" in styles
|
|
42
|
+
assert "shared-def456.css" in styles
|
|
43
|
+
assert 'rel="stylesheet"' in styles
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_prod_mode_passes_through_absolute_urls(tmp_path):
|
|
47
|
+
import json
|
|
48
|
+
path = tmp_path / "manifest.json"
|
|
49
|
+
path.write_text(json.dumps({
|
|
50
|
+
"src/main.ts": {"file": "https://cdn.example.com/main.js", "css": ["https://cdn.example.com/main.css"]},
|
|
51
|
+
}))
|
|
52
|
+
with override_settings(DEBUG=False, VITE={"manifest_path": str(path)}, RELEASE_VERSION="v1"):
|
|
53
|
+
scripts = _render('{% load vite %}{% vite_scripts "src/main.ts" %}')
|
|
54
|
+
assert "https://cdn.example.com/main.js" in scripts
|
|
55
|
+
assert "/static/https" not in scripts
|