From b1b2a688a8602e0f1d45929230e3d3d99f2c6f24 Mon Sep 17 00:00:00 2001 From: rash Date: Sun, 22 Feb 2026 17:40:25 +0100 Subject: [PATCH] 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 --- package.json | 5 +- src/pretix/_build.py | 6 ++ src/pretix/presale/views/widget.py | 60 ++++++++++------ src/pretix/settings.py | 1 + .../static/pretixpresale/widget/src/i18n.ts | 24 ++++--- .../pretixpresale/widget/vite.config.ts | 17 +++-- src/tests/e2e/conftest.py | 69 ++++++++++++++++--- vite.config.js | 6 +- 8 files changed, 139 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 256f7d5161..516a33594e 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ }, "scripts": { "dev": "vite", - "build": "vite build", "dev:widget": "vite src/pretix/static/pretixpresale/widget", - "build:widget": "vite build --config src/pretix/static/pretixpresale/widget/vite.config.ts", + "build": "npm run build:control -s && npm run build:widget -s", + "build:control": "vite build", + "build:widget": "vite build src/pretix/static/pretixpresale/widget", "lint:eslint": "eslint . --ext .js,.ts,.vue", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/pretix/_build.py b/src/pretix/_build.py index de9e2900c3..d4e9bdd516 100644 --- a/src/pretix/_build.py +++ b/src/pretix/_build.py @@ -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) diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py index 9de49b067b..5adff841bb 100644 --- a/src/pretix/presale/views/widget.py +++ b/src/pretix/presale/views/widget.py @@ -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'] = '*' diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 0271e7294a..7f9646de6f 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -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') diff --git a/src/pretix/static/pretixpresale/widget/src/i18n.ts b/src/pretix/static/pretixpresale/widget/src/i18n.ts index ad2a40d01b..e4caf3b289 100644 --- a/src/pretix/static/pretixpresale/widget/src/i18n.ts +++ b/src/pretix/static/pretixpresale/widget/src/i18n.ts @@ -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'), diff --git a/src/pretix/static/pretixpresale/widget/vite.config.ts b/src/pretix/static/pretixpresale/widget/vite.config.ts index 62e05e75f7..895cfc4ae2 100644 --- a/src/pretix/static/pretixpresale/widget/vite.config.ts +++ b/src/pretix/static/pretixpresale/widget/vite.config.ts @@ -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'), + }), + }, +})) diff --git a/src/tests/e2e/conftest.py b/src/tests/e2e/conftest.py index eabadfec92..35b398c97b 100644 --- a/src/tests/e2e/conftest.py +++ b/src/tests/e2e/conftest.py @@ -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'' + else: + widget_js = f"{base_url}/widget/v2.en.js" + script_tag = f'' # Build extra attributes from query params extra_attrs = '' @@ -1097,7 +1145,7 @@ def _register_widget_test_view(): - + {script_tag} """ 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'' + else: + widget_js = f"{base_url}/widget/v2.en.js" + script_tag = f'' # Build extra attributes from query params extra_attrs = '' @@ -1135,7 +1188,7 @@ def _register_widget_test_view(): {button_text} - + {script_tag} """ resp = HttpResponse(html, content_type='text/html') diff --git a/vite.config.js b/vite.config.js index 1f9fa6f2ed..9e5ba0f5b9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,3 +1,5 @@ +// vite build config for control UI +// widget has its own config, see src/pretix/static/pretixpresale/widget/vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' @@ -8,11 +10,11 @@ export default defineConfig({ ], build: { manifest: true, - outDir: path.resolve(__dirname, '../../static.dist/vite'), + outDir: path.resolve(__dirname, 'src/pretix/static.dist/vite/control'), rollupOptions: { input: { // 'webcheckin/main': path.resolve(__dirname, '../plugins/webcheckin/static/pretixplugins/webcheckin/main.js'), - 'checkinrules/main': path.resolve(__dirname, '../pretixcontrol/js/ui/checkinrules/index.ts') + 'checkinrules/main': path.resolve(__dirname, 'src/pretix/static/pretixcontrol/js/ui/checkinrules/index.ts') }, } },