working vite widget setup for prod (untested), local dev (with or without dev server) and pytests, with flags for running the old version or the vite version

This commit is contained in:
rash
2026-02-22 17:40:25 +01:00
parent 3f92868dba
commit b1b2a688a8
8 changed files with 139 additions and 49 deletions

View File

@@ -28,6 +28,7 @@ from setuptools.command.build import build
from setuptools.command.build_ext import build_ext
here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.abspath(os.path.join(here, '..', '..'))
npm_installed = False
@@ -43,6 +44,10 @@ def npm_install():
npm_installed = True
def npm_build():
subprocess.check_call('npm run build', shell=True, cwd=project_root)
class CustomBuild(build):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
@@ -62,6 +67,7 @@ class CustomBuild(build):
settings.COMPRESS_OFFLINE = True
npm_install()
npm_build()
management.call_command('compilemessages', verbosity=1)
management.call_command('compilejsi18n', verbosity=1)
management.call_command('collectstatic', verbosity=1, interactive=False)

View File

@@ -123,7 +123,8 @@ def widget_css_etag(request, version, **kwargs):
def widget_js_etag(request, version, lang, **kwargs):
gs = GlobalSettingsObject()
return gs.settings.get('widget_checksum_{}_{}'.format(version, lang))
variant = 'vite' if getattr(settings, 'PRETIX_WIDGET_VITE', False) else 'legacy'
return gs.settings.get('widget_checksum_{}_{}_{}'.format(version, lang, variant))
@gzip_page
@@ -152,13 +153,16 @@ def widget_css(request, version, **kwargs):
return resp
def generate_widget_js(version, lang):
def generate_widget_js(version, lang, use_vite=False):
code = []
with language(lang):
# Provide isolation
code.append('(function (siteglobals) {\n')
code.append('var module = {}, exports = {};\n')
code.append('var lang = "%s";\n' % lang)
if use_vite:
code.append('const LANG = "%s";\n' % lang)
else:
code.append('var lang = "%s";\n' % lang)
c = JavaScriptCatalog()
c.translation = DjangoTranslation(lang, domain='djangojs')
@@ -179,20 +183,25 @@ def generate_widget_js(version, lang):
'plural': plural,
})
i18n_js = template.render(context)
i18n_js = i18n_js.replace('for (const ', 'for (var ') # remove if we really want to break IE11 for good
i18n_js = i18n_js.replace(r"value.includes(", r"-1 != value.indexOf(") # remove if we really want to break IE11 for good
code.append(i18n_js)
files = [
'vuejs/vue.js' if settings.DEBUG else 'vuejs/vue.min.js',
'pretixpresale/js/widget/docready.js',
'pretixpresale/js/widget/floatformat.js',
'pretixpresale/js/widget/widget.js' if version == version_max else 'pretixpresale/js/widget/widget.v{}.js'.format(version),
]
for fname in files:
f = finders.find(fname)
with open(f, 'r', encoding='utf-8') as fp:
if use_vite:
vite_js = finders.find('vite/widget/widget.js')
if not vite_js:
raise FileNotFoundError('Vite widget build not found. Run: npm run build:widget')
with open(vite_js, 'r', encoding='utf-8') as fp:
code.append(fp.read())
else:
files = [
'vuejs/vue.js' if settings.DEBUG else 'vuejs/vue.min.js',
'pretixpresale/js/widget/docready.js',
'pretixpresale/js/widget/floatformat.js',
'pretixpresale/js/widget/widget.js' if version == version_max else 'pretixpresale/js/widget/widget.v{}.js'.format(version),
]
for fname in files:
f = finders.find(fname)
with open(f, 'r', encoding='utf-8') as fp:
code.append(fp.read())
if settings.DEBUG:
code.append('})(this);\n')
@@ -213,15 +222,22 @@ def widget_js(request, version, lang, **kwargs):
if version < version_min:
version = version_min
cached_js = cache.get('widget_js_data_v{}_{}'.format(version, lang))
use_vite = getattr(settings, 'PRETIX_WIDGET_VITE', False)
variant = 'vite' if use_vite else 'legacy'
cache_prefix = 'widget_js_data_v{}_{}_{}'.format(version, lang, variant)
cached_js = cache.get(cache_prefix)
if cached_js and not settings.DEBUG:
resp = HttpResponse(cached_js, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
settings_key = 'widget_file_v{}_{}_{}'.format(version, lang, variant)
checksum_key = 'widget_checksum_v{}_{}_{}'.format(version, lang, variant)
gs = GlobalSettingsObject()
fname = gs.settings.get('widget_file_v{}_{}'.format(version, lang))
fname = gs.settings.get(settings_key)
resp = None
if fname and not settings.DEBUG:
if isinstance(fname, File):
@@ -229,21 +245,21 @@ def widget_js(request, version, lang, **kwargs):
try:
data = default_storage.open(fname).read()
resp = HttpResponse(data, content_type='text/javascript')
cache.set('widget_js_data_v{}_{}'.format(version, lang), data, 3600 * 4)
cache.set(cache_prefix, data, 3600 * 4)
except:
logger.exception('Failed to open widget.js')
if not resp:
data = generate_widget_js(version, lang).encode()
data = generate_widget_js(version, lang, use_vite=use_vite).encode()
checksum = hashlib.sha1(data).hexdigest()
if not settings.DEBUG:
newname = default_storage.save(
'widget/widget.{}.{}.{}.js'.format(version, lang, checksum),
'widget/widget.{}.{}.{}.{}.js'.format(version, lang, variant, checksum),
ContentFile(data)
)
gs.settings.set('widget_file_v{}_{}'.format(version, lang), 'file://' + newname)
gs.settings.set('widget_checksum_v{}_{}'.format(version, lang), checksum)
cache.set('widget_js_data_v{}_{}'.format(version, lang), data, 3600 * 4)
gs.settings.set(settings_key, 'file://' + newname)
gs.settings.set(checksum_key, checksum)
cache.set(cache_prefix, data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'

View File

@@ -871,3 +871,4 @@ VITE_DEV_SERVER_PORT = 5173
VITE_DEV_SERVER = f"http://localhost:{VITE_DEV_SERVER_PORT}"
VITE_DEV_MODE = DEBUG
VITE_IGNORE = False # Used to ignore `collectstatic`/`rebuild`
PRETIX_WIDGET_VITE = os.environ.get('PRETIX_WIDGET_VITE', '') not in ('', '0')

View File

@@ -1,9 +1,8 @@
// Internationalization strings for the pretix widget
// Django's i18n file expects `this` to be the global object, but ES modules
// have `this` as undefined. Import as raw text and execute with a local context.
// TODO hack
import djangoI18nScript from '../../../jsi18n/en/djangojs.js?raw'
// In production, widget.py injects the `django` global before this script loads.
// In dev mode, Django's i18n file expects `this` to be the global object, but
// ES modules have `this` as undefined — so we import as raw text and execute
// with a local context.
interface Django {
pgettext: (context: string, text: string) => string
@@ -12,10 +11,17 @@ interface Django {
get_format: (formatType: string) => string | number
}
// Create a local context object to capture django without polluting window
const context: { django?: Django } = {}
new Function(djangoI18nScript).call(context)
const django = context.django!
let django: Django
if (import.meta.env.DEV) {
// TODO this does not actually grab the correct language strings
const raw = (await import(`../../../jsi18n/${LANG}/djangojs.js?raw`)).default
const context: { django?: Django } = {}
new Function(raw).call(context)
django = context.django!
} else {
django = (globalThis as any).django
}
export const STRINGS = {
quantity: django.pgettext('widget', 'Quantity'),

View File

@@ -1,7 +1,10 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
export default defineConfig(({ mode }) => ({
server: {
port: 5180
},
plugins: [
vue()
],
@@ -11,8 +14,8 @@ export default defineConfig({
},
},
build: {
manifest: true,
outDir: import.meta.dirname + '/../../../../../static.dist/vite/widget',
minify: false, // django will do minification
outDir: import.meta.dirname + '/../../../static.dist/vite/widget',
rollupOptions: {
input: {
main: import.meta.dirname + '/src/main.ts',
@@ -28,6 +31,8 @@ export default defineConfig({
exclude: ['moment', 'jquery']
},
define: {
LANG: JSON.stringify(process.env.PRETIX_WIDGET_LANG || 'en')
}
})
...(mode === 'development' && {
LANG: JSON.stringify(process.env.PRETIX_WIDGET_LANG || 'en'),
}),
},
}))

View File

@@ -4,10 +4,16 @@ E2E Test Configuration for Pretix Widget with Playwright
This module provides pytest fixtures for end-to-end testing of the pretix widget
using Playwright. It integrates Playwright with Django's test infrastructure.
"""
# TODO dev server websocket does not work, but is this relevant?
import os
import subprocess
import pytest
from decimal import Decimal
from datetime import date, datetime, timezone, timedelta
from urllib.request import urlopen
from urllib.error import URLError
from playwright.sync_api import Browser, BrowserContext, Page, expect
from django_scopes import scopes_disabled
@@ -29,6 +35,43 @@ def _future_dt(days=30, hour=10, minute=0):
return datetime(d.year, d.month, d.day, hour, minute, tzinfo=timezone.utc)
# ============================================================================
# Widget Asset Configuration (old Vue2 / new Vite / Vite dev server)
# ============================================================================
PROJECT_ROOT = os.path.join(
os.path.dirname(__file__),
'../../..'
)
VITE_DEV_PORT = 5180
@pytest.fixture(scope="session", autouse=True)
def _widget_assets():
"""
Build or check the widget JS depending on env vars.
- Default: old Vue2 widget (no build needed)
- PRETIX_WIDGET_VITE=1: run vite build, Django serves the output
- PRETIX_WIDGET_VITE_DEV=1: uses your already-running vite dev server
"""
if os.environ.get("PRETIX_WIDGET_VITE_DEV"):
try:
urlopen(f'http://localhost:{VITE_DEV_PORT}/', timeout=2)
except (URLError, OSError):
raise RuntimeError(
f'PRETIX_WIDGET_VITE_DEV is set but no Vite dev server found on port {VITE_DEV_PORT}. '
f'Start it with: npm run dev:widget'
)
yield
elif os.environ.get("PRETIX_WIDGET_VITE"):
subprocess.check_call(['npm', 'run', 'build:widget'], cwd=PROJECT_ROOT)
yield
else:
yield # Old widget, no build needed
# ============================================================================
# Playwright Configuration
# ============================================================================
@@ -86,12 +129,12 @@ def live_server_url(live_server, settings):
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
# Fix SITE_URL to point to live server instead of example.com.
# This makes build_absolute_uri() return localhost URLs so the widget's
# target_url (from the API response) resolves to the live server.
# Without this, form submissions (cart/add) go to example.com.
settings.SITE_URL = live_server.url
# Enable Vite widget if requested via env var
if os.environ.get("PRETIX_WIDGET_VITE") or os.environ.get("PRETIX_WIDGET_VITE_DEV"):
settings.PRETIX_WIDGET_VITE = True
return live_server.url
# ============================================================================
@@ -1070,7 +1113,12 @@ def _register_widget_test_view():
base_url = f"{request.scheme}://{request.get_host()}"
event_url = f"{base_url}/{organizer}/{event}/"
widget_css = f"{base_url}/{organizer}/{event}/widget/v2.css"
widget_js = f"{base_url}/widget/v2.en.js"
if os.environ.get("PRETIX_WIDGET_VITE_DEV"):
script_tag = f'<script type="module" src="http://localhost:{VITE_DEV_PORT}/src/main.ts"></script>'
else:
widget_js = f"{base_url}/widget/v2.en.js"
script_tag = f'<script type="text/javascript" src="{widget_js}" async crossorigin></script>'
# Build extra attributes from query params
extra_attrs = ''
@@ -1097,7 +1145,7 @@ def _register_widget_test_view():
</head>
<body>
<pretix-widget event="{event_url}"{extra_attrs}></pretix-widget>
<script type="text/javascript" src="{widget_js}" async crossorigin></script>
{script_tag}
</body>
</html>"""
resp = HttpResponse(html, content_type='text/html')
@@ -1111,7 +1159,12 @@ def _register_widget_test_view():
base_url = f"{request.scheme}://{request.get_host()}"
event_url = f"{base_url}/{organizer}/{event}/"
widget_css = f"{base_url}/{organizer}/{event}/widget/v2.css"
widget_js = f"{base_url}/widget/v2.en.js"
if os.environ.get("PRETIX_WIDGET_VITE_DEV"):
script_tag = f'<script type="module" src="http://localhost:{VITE_DEV_PORT}/src/main.ts"></script>'
else:
widget_js = f"{base_url}/widget/v2.en.js"
script_tag = f'<script type="text/javascript" src="{widget_js}" async crossorigin></script>'
# Build extra attributes from query params
extra_attrs = ''
@@ -1135,7 +1188,7 @@ def _register_widget_test_view():
</head>
<body>
<pretix-button event="{event_url}"{extra_attrs}>{button_text}</pretix-button>
<script type="text/javascript" src="{widget_js}" async crossorigin></script>
{script_tag}
</body>
</html>"""
resp = HttpResponse(html, content_type='text/html')