From ca328456d3a6d87f2338801fb96a2bf8d0f4bcd7 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 02:31:22 +0530 Subject: [PATCH 1/8] feat(frontend): add Vite bundling option for HTMX+Tailwind Add production-ready asset bundling as an alternative to CDN-based delivery for the HTMX+Tailwind frontend option. Changes: - Add frontend_bundling copier question (vite/cdn, default: vite) - Create Vite frontend setup (package.json, vite/tailwind/postcss configs) - Add django-vite integration for asset loading - Add vite service to docker-compose for development HMR - Add multi-stage Dockerfile build for production assets - Update .gitignore for node_modules and build artifacts The CDN option remains available for quick prototypes, while Vite provides bundled assets without external dependencies for production. --- copier.yml | 9 ++++++ template/.gitignore.jinja | 9 ++++++ template/Dockerfile.jinja | 29 +++++++++++++++++++ template/config/settings/base.py.jinja | 16 ++++++++++ template/docker-compose.yml.jinja | 14 +++++++++ ... == 'vite' %}package.json{% endif %}.jinja | 21 ++++++++++++++ ...vite' %}postcss.config.js{% endif %}.jinja | 6 ++++ ...dling == 'vite' %}main.js{% endif %}.jinja | 11 +++++++ ...ng == 'vite' %}styles.css{% endif %}.jinja | 3 ++ ...ite' %}tailwind.config.js{% endif %}.jinja | 11 +++++++ ...= 'vite' %}vite.config.js{% endif %}.jinja | 20 +++++++++++++ ...tmx-tailwind' %}base.html{% endif %}.jinja | 8 ++++- ...'poetry' %}pyproject.toml{% endif %}.jinja | 3 ++ ... == 'uv' %}pyproject.toml{% endif %}.jinja | 3 ++ 14 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 template/frontend/{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja create mode 100644 template/frontend/{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja create mode 100644 template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja create mode 100644 template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja create mode 100644 template/frontend/{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja create mode 100644 template/frontend/{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja diff --git a/copier.yml b/copier.yml index 9198a3a..de8d9f6 100644 --- a/copier.yml +++ b/copier.yml @@ -118,6 +118,15 @@ frontend: {%- elif project_type == 'internal-tool' -%}htmx-tailwind {%- else -%}none{%- endif -%} +frontend_bundling: + type: str + help: Frontend asset bundling + when: "{{ frontend == 'htmx-tailwind' }}" + choices: + Vite (Production-ready, bundled assets): vite + CDN (Simple, no build step): cdn + default: "vite" + # Async & Tasks background_tasks: type: str diff --git a/template/.gitignore.jinja b/template/.gitignore.jinja index 606eb46..340f439 100644 --- a/template/.gitignore.jinja +++ b/template/.gitignore.jinja @@ -31,6 +31,9 @@ db.sqlite3-journal /media /staticfiles /static_root +{% if frontend_bundling == 'vite' -%} +/static/dist +{% endif -%} # Environment .env @@ -42,6 +45,12 @@ db.sqlite3-journal .venv/ poetry.lock {% endif -%} +{% if frontend_bundling == 'vite' -%} + +# Node +node_modules/ +frontend/node_modules/ +{% endif -%} # IDEs .vscode/ diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index 5c8c670..e418998 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -1,4 +1,23 @@ # syntax=docker/dockerfile:1 +{% if frontend_bundling == 'vite' -%} +# Build frontend assets +FROM node:20-slim AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend files +COPY frontend/package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy rest of frontend +COPY frontend/ ./ + +# Build assets +RUN npm run build + +{% endif -%} FROM python:{{ python_version }}-slim as base # Set environment variables @@ -42,6 +61,11 @@ RUN uv sync --no-dev # Copy rest of application COPY . . +{% if frontend_bundling == 'vite' -%} +# Copy built frontend assets +COPY --from=frontend-builder /app/static/dist ./static/dist +{% endif -%} + {% else -%} # Install poetry RUN pip install poetry==1.7.0 @@ -61,6 +85,11 @@ RUN poetry config virtualenvs.create false \ # Copy rest of application COPY . . + +{% if frontend_bundling == 'vite' -%} +# Copy built frontend assets +COPY --from=frontend-builder /app/static/dist ./static/dist +{% endif -%} {% endif -%} # Change ownership diff --git a/template/config/settings/base.py.jinja b/template/config/settings/base.py.jinja index 8bbbd10..92facba 100644 --- a/template/config/settings/base.py.jinja +++ b/template/config/settings/base.py.jinja @@ -44,6 +44,9 @@ DJANGO_APPS = [ ] THIRD_PARTY_APPS = [ +{% if frontend_bundling == 'vite' -%} + "django_vite", +{% endif -%} {% if api_style in ['drf', 'both'] -%} "rest_framework", "drf_spectacular", @@ -242,6 +245,19 @@ STORAGES = { MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" +{% if frontend_bundling == 'vite' -%} +# Django Vite +DJANGO_VITE = { + "default": { + "dev_mode": DEBUG, + "manifest_path": BASE_DIR / "static" / "dist" / "manifest.json", + "static_url_prefix": "dist/", + "dev_server_host": "localhost", + "dev_server_port": 5173, + } +} +{% endif -%} + {% if media_storage == 'aws-s3' -%} # AWS S3 Storage AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default="") diff --git a/template/docker-compose.yml.jinja b/template/docker-compose.yml.jinja index 427cda6..d839c87 100644 --- a/template/docker-compose.yml.jinja +++ b/template/docker-compose.yml.jinja @@ -65,6 +65,20 @@ services: ports: - "1025:1025" - "8025:8025" +{% if frontend_bundling == 'vite' %} + + vite: + image: node:20-slim + working_dir: /app/frontend + volumes: + - ./frontend:/app/frontend + - ./static:/app/static + ports: + - "5173:5173" + command: sh -c "npm install && npm run dev" + environment: + - VITE_DEV_SERVER_HOST=0.0.0.0 +{% endif %} {% if background_tasks in ['temporal', 'both'] %} temporal: diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja b/template/frontend/{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja new file mode 100644 index 0000000..b830f10 --- /dev/null +++ b/template/frontend/{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja @@ -0,0 +1,21 @@ +{ + "name": "{{ project_slug }}-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "htmx.org": "^1.9.10", + "alpinejs": "^3.13.3" + }, + "devDependencies": { + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "vite": "^5.0.10" + } +} diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja b/template/frontend/{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/template/frontend/{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja b/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja new file mode 100644 index 0000000..d0392e4 --- /dev/null +++ b/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja @@ -0,0 +1,11 @@ +// Import styles +import "./styles.css"; + +// Import HTMX +import htmx from "htmx.org"; +window.htmx = htmx; + +// Import Alpine.js +import Alpine from "alpinejs"; +window.Alpine = Alpine; +Alpine.start(); diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja b/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja b/template/frontend/{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja new file mode 100644 index 0000000..bf4b3eb --- /dev/null +++ b/template/frontend/{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "../templates/**/*.html", + "./src/**/*.js", + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja b/template/frontend/{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja new file mode 100644 index 0000000..ee42010 --- /dev/null +++ b/template/frontend/{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja @@ -0,0 +1,20 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; + +export default defineConfig({ + base: "/static/", + build: { + manifest: "manifest.json", + outDir: resolve(__dirname, "../static/dist"), + rollupOptions: { + input: { + main: resolve(__dirname, "src/main.js"), + }, + }, + }, + server: { + host: "0.0.0.0", + port: 5173, + origin: "http://localhost:5173", + }, +}); diff --git a/template/templates/{% if frontend == 'htmx-tailwind' %}base.html{% endif %}.jinja b/template/templates/{% if frontend == 'htmx-tailwind' %}base.html{% endif %}.jinja index 1a033b3..a65761f 100644 --- a/template/templates/{% if frontend == 'htmx-tailwind' %}base.html{% endif %}.jinja +++ b/template/templates/{% if frontend == 'htmx-tailwind' %}base.html{% endif %}.jinja @@ -1,10 +1,15 @@ - +{% if frontend_bundling == 'vite' %}{% raw %}{% load django_vite %}{% endraw %} +{% endif %} {% raw %}{% block title %}{% endraw %}{{ project_name }}{% raw %}{% endblock %}{% endraw %} +{% if frontend_bundling == 'vite' %} + {% raw %}{% vite_hmr_client %}{% endraw %} + {% raw %}{% vite_asset 'src/main.js' %}{% endraw %} +{% else %} @@ -13,6 +18,7 @@ +{% endif %} {% raw %}{% block extra_head %}{% endblock %}{% endraw %} diff --git a/template/{% if dependency_manager == 'poetry' %}pyproject.toml{% endif %}.jinja b/template/{% if dependency_manager == 'poetry' %}pyproject.toml{% endif %}.jinja index 6825453..ba56b54 100644 --- a/template/{% if dependency_manager == 'poetry' %}pyproject.toml{% endif %}.jinja +++ b/template/{% if dependency_manager == 'poetry' %}pyproject.toml{% endif %}.jinja @@ -91,6 +91,9 @@ django-opensearch-dsl = "^0.6.0" {% if use_i18n -%} django-parler = "^2.3.0" {% endif -%} +{% if frontend_bundling == 'vite' -%} +django-vite = "^3.0.0" +{% endif -%} django-extensions = "^3.2.0" python-dotenv = "^1.0.0" django-alive = "^1.3.0" diff --git a/template/{% if dependency_manager == 'uv' %}pyproject.toml{% endif %}.jinja b/template/{% if dependency_manager == 'uv' %}pyproject.toml{% endif %}.jinja index 8fa2b7a..f6b4c4e 100644 --- a/template/{% if dependency_manager == 'uv' %}pyproject.toml{% endif %}.jinja +++ b/template/{% if dependency_manager == 'uv' %}pyproject.toml{% endif %}.jinja @@ -91,6 +91,9 @@ dependencies = [ {% endif -%} {% if use_i18n -%} "django-parler>=2.3.0", +{% endif -%} +{% if frontend_bundling == 'vite' -%} + "django-vite>=3.0.0", {% endif -%} "django-extensions>=3.2.0", "python-dotenv>=1.0.0", From afe8e4e3ffe1552280b205032b447ca888651f75 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 02:32:59 +0530 Subject: [PATCH 2/8] docs: add Vite bundling documentation - Update CHANGELOG.md with frontend bundling option - Update frontend-options.md with Vite vs CDN comparison --- CHANGELOG.md | 1 + docs/features/frontend-options.md | 33 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad863a..46f733f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### API & Frontend Options - API frameworks: Django REST Framework, Strawberry GraphQL, both, or none - Frontend: HTMX + Tailwind CSS, Next.js, or headless +- Frontend bundling for HTMX+Tailwind: Vite (production-ready) or CDN (simple) - Authentication: django-allauth, JWT, or both #### Background Task Processing diff --git a/docs/features/frontend-options.md b/docs/features/frontend-options.md index aca9e83..fc51383 100644 --- a/docs/features/frontend-options.md +++ b/docs/features/frontend-options.md @@ -16,6 +16,39 @@ Modern, minimal-JavaScript approach with server-rendered HTML. - **Tailwind CSS** for styling - **Alpine.js** for lightweight JavaScript +#### Asset Bundling Options + +When you select HTMX + Tailwind, you can choose how assets are delivered: + +| Option | Best For | Build Step | External Dependencies | +|--------|----------|------------|----------------------| +| **Vite** (default) | Production apps | Yes | None | +| **CDN** | Prototypes, MVPs | No | Yes (3 CDNs) | + +**Vite (Recommended for Production)** + +- Bundles Tailwind CSS, HTMX, and Alpine.js locally +- No external CDN dependencies in production +- Proper CSS purging for smaller bundles +- Hot Module Replacement (HMR) in development +- Uses `django-vite` for seamless Django integration + +Development: +```bash +docker-compose up # Starts Django + Vite dev server +``` + +Production: +- Assets are built during Docker image build +- Served via WhiteNoise from `/static/dist/` + +**CDN (Simple, No Build Step)** + +- Assets loaded from external CDNs (tailwindcss, unpkg, jsdelivr) +- Zero configuration needed +- Good for quick prototypes +- Not recommended for production (external dependencies) + ### Next.js Full-featured React framework for building modern web applications. From 32d5aebb52e2f876e40951eae7ff98a595e5a02b Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 02:49:09 +0530 Subject: [PATCH 3/8] fix: prevent Vite files conflict with Next.js frontend - Add frontend == 'htmx-tailwind' condition to Vite template files - Update tests to check for Vite (default) and CDN modes separately - Add new test_htmx_frontend_cdn_mode test --- ...== 'vite' %}package.json{% endif %}.jinja} | 0 ...ite' %}postcss.config.js{% endif %}.jinja} | 0 ...ling == 'vite' %}main.js{% endif %}.jinja} | 0 ...g == 'vite' %}styles.css{% endif %}.jinja} | 0 ...te' %}tailwind.config.js{% endif %}.jinja} | 0 ... 'vite' %}vite.config.js{% endif %}.jinja} | 0 tests/test_generation.py | 22 ++++++++++++++++++- 7 files changed, 21 insertions(+), 1 deletion(-) rename template/frontend/{{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja => {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}package.json{% endif %}.jinja} (100%) rename template/frontend/{{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja => {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja} (100%) rename template/frontend/{{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja => {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}src{% endif %}/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}main.js{% endif %}.jinja} (100%) rename template/frontend/{{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja => {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}src{% endif %}/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}styles.css{% endif %}.jinja} (100%) rename template/frontend/{{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja => {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja} (100%) rename template/frontend/{{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja => {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja} (100%) diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja b/template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}package.json{% endif %}.jinja similarity index 100% rename from template/frontend/{% if frontend_bundling == 'vite' %}package.json{% endif %}.jinja rename to template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}package.json{% endif %}.jinja diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja b/template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja similarity index 100% rename from template/frontend/{% if frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja rename to template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}postcss.config.js{% endif %}.jinja diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja b/template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}src{% endif %}/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}main.js{% endif %}.jinja similarity index 100% rename from template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}main.js{% endif %}.jinja rename to template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}src{% endif %}/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}main.js{% endif %}.jinja diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja b/template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}src{% endif %}/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}styles.css{% endif %}.jinja similarity index 100% rename from template/frontend/{% if frontend_bundling == 'vite' %}src{% endif %}/{% if frontend_bundling == 'vite' %}styles.css{% endif %}.jinja rename to template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}src{% endif %}/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}styles.css{% endif %}.jinja diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja b/template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja similarity index 100% rename from template/frontend/{% if frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja rename to template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}tailwind.config.js{% endif %}.jinja diff --git a/template/frontend/{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja b/template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja similarity index 100% rename from template/frontend/{% if frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja rename to template/frontend/{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}vite.config.js{% endif %}.jinja diff --git a/tests/test_generation.py b/tests/test_generation.py index 2a19132..010fe65 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -204,16 +204,36 @@ def test_no_api_excludes_frameworks(generate): def test_htmx_frontend_templates_generated(generate): - """Test that HTMX frontend generates templates.""" + """Test that HTMX frontend generates templates with Vite (default).""" project = generate(frontend="htmx-tailwind") templates_dir = project / "templates" assert (templates_dir / "base.html").exists() + base_html = (templates_dir / "base.html").read_text() + assert "{% block" in base_html + # Vite is the default, so check for vite tags + assert "django_vite" in base_html + assert "vite_asset" in base_html + + # Vite frontend files should exist + frontend_dir = project / "frontend" + assert (frontend_dir / "package.json").exists() + assert (frontend_dir / "vite.config.js").exists() + assert (frontend_dir / "tailwind.config.js").exists() + + +def test_htmx_frontend_cdn_mode(generate): + """Test that HTMX frontend with CDN mode uses CDN links.""" + project = generate(frontend="htmx-tailwind", frontend_bundling="cdn") + + templates_dir = project / "templates" base_html = (templates_dir / "base.html").read_text() assert "{% block" in base_html assert "tailwindcss" in base_html assert "htmx" in base_html + # Should not have vite tags + assert "django_vite" not in base_html def test_nextjs_frontend_generated(generate): From 42803a0eca897f4ca5b3d18e58aa238f705811b3 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 02:52:05 +0530 Subject: [PATCH 4/8] fix: use env var for Vite dev server host in Docker - Make VITE_DEV_SERVER_HOST configurable via environment variable - Pass 'vite' hostname to Django web service in docker-compose - Remove unused env var from vite service - Defaults to 'localhost' for non-Docker development --- template/config/settings/base.py.jinja | 2 +- template/docker-compose.yml.jinja | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/template/config/settings/base.py.jinja b/template/config/settings/base.py.jinja index 92facba..ed76d22 100644 --- a/template/config/settings/base.py.jinja +++ b/template/config/settings/base.py.jinja @@ -252,7 +252,7 @@ DJANGO_VITE = { "dev_mode": DEBUG, "manifest_path": BASE_DIR / "static" / "dist" / "manifest.json", "static_url_prefix": "dist/", - "dev_server_host": "localhost", + "dev_server_host": env("VITE_DEV_SERVER_HOST", default="localhost"), "dev_server_port": 5173, } } diff --git a/template/docker-compose.yml.jinja b/template/docker-compose.yml.jinja index d839c87..3aaf3e0 100644 --- a/template/docker-compose.yml.jinja +++ b/template/docker-compose.yml.jinja @@ -26,6 +26,9 @@ services: - EMAIL_PORT=1025 {% if dependency_manager == 'uv' %} - UV_NO_CACHE=1 +{% endif %} +{% if frontend_bundling == 'vite' %} + - VITE_DEV_SERVER_HOST=vite {% endif %} env_file: - .env @@ -76,8 +79,6 @@ services: ports: - "5173:5173" command: sh -c "npm install && npm run dev" - environment: - - VITE_DEV_SERVER_HOST=0.0.0.0 {% endif %} {% if background_tasks in ['temporal', 'both'] %} From 43ef2f5fdf9a4ab1edbf903d5f87efaca4d7fc51 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 03:08:25 +0530 Subject: [PATCH 5/8] fix: handle undefined frontend_bundling variable - Add 'frontend == htmx-tailwind' check to all frontend_bundling conditionals - Prevents template errors when frontend != htmx-tailwind - Add mkdir -p /app/static before npm build in Dockerfile - Enhance CDN mode test to verify no frontend files are created --- template/.gitignore.jinja | 4 ++-- template/Dockerfile.jinja | 10 +++++----- template/config/settings/base.py.jinja | 4 ++-- template/docker-compose.yml.jinja | 4 ++-- tests/test_generation.py | 6 ++++++ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/template/.gitignore.jinja b/template/.gitignore.jinja index 340f439..975583c 100644 --- a/template/.gitignore.jinja +++ b/template/.gitignore.jinja @@ -31,7 +31,7 @@ db.sqlite3-journal /media /staticfiles /static_root -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} /static/dist {% endif -%} @@ -45,7 +45,7 @@ db.sqlite3-journal .venv/ poetry.lock {% endif -%} -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} # Node node_modules/ diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index e418998..0abffb3 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} # Build frontend assets FROM node:20-slim AS frontend-builder @@ -14,8 +14,8 @@ RUN npm ci # Copy rest of frontend COPY frontend/ ./ -# Build assets -RUN npm run build +# Create output directory and build assets +RUN mkdir -p /app/static && npm run build {% endif -%} FROM python:{{ python_version }}-slim as base @@ -61,7 +61,7 @@ RUN uv sync --no-dev # Copy rest of application COPY . . -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} # Copy built frontend assets COPY --from=frontend-builder /app/static/dist ./static/dist {% endif -%} @@ -86,7 +86,7 @@ RUN poetry config virtualenvs.create false \ # Copy rest of application COPY . . -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} # Copy built frontend assets COPY --from=frontend-builder /app/static/dist ./static/dist {% endif -%} diff --git a/template/config/settings/base.py.jinja b/template/config/settings/base.py.jinja index ed76d22..d16bde6 100644 --- a/template/config/settings/base.py.jinja +++ b/template/config/settings/base.py.jinja @@ -44,7 +44,7 @@ DJANGO_APPS = [ ] THIRD_PARTY_APPS = [ -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} "django_vite", {% endif -%} {% if api_style in ['drf', 'both'] -%} @@ -245,7 +245,7 @@ STORAGES = { MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" -{% if frontend_bundling == 'vite' -%} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} # Django Vite DJANGO_VITE = { "default": { diff --git a/template/docker-compose.yml.jinja b/template/docker-compose.yml.jinja index 3aaf3e0..dc3ba19 100644 --- a/template/docker-compose.yml.jinja +++ b/template/docker-compose.yml.jinja @@ -27,7 +27,7 @@ services: {% if dependency_manager == 'uv' %} - UV_NO_CACHE=1 {% endif %} -{% if frontend_bundling == 'vite' %} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} - VITE_DEV_SERVER_HOST=vite {% endif %} env_file: @@ -68,7 +68,7 @@ services: ports: - "1025:1025" - "8025:8025" -{% if frontend_bundling == 'vite' %} +{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} vite: image: node:20-slim diff --git a/tests/test_generation.py b/tests/test_generation.py index 010fe65..0667ba6 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -235,6 +235,12 @@ def test_htmx_frontend_cdn_mode(generate): # Should not have vite tags assert "django_vite" not in base_html + # CDN mode should NOT create frontend build files + frontend_dir = project / "frontend" + assert not (frontend_dir / "package.json").exists() + assert not (frontend_dir / "vite.config.js").exists() + assert not (frontend_dir / "tailwind.config.js").exists() + def test_nextjs_frontend_generated(generate): """Test that Next.js frontend is generated.""" From 36f505c2e774272785d31dded723ad399edf61e2 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 23:09:25 +0530 Subject: [PATCH 6/8] docs: expand Vite development and troubleshooting guide - Add local development instructions (without Docker) - Add production build commands - Add troubleshooting table for common issues --- docs/features/frontend-options.md | 36 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/features/frontend-options.md b/docs/features/frontend-options.md index fc51383..9492c1c 100644 --- a/docs/features/frontend-options.md +++ b/docs/features/frontend-options.md @@ -33,14 +33,40 @@ When you select HTMX + Tailwind, you can choose how assets are delivered: - Hot Module Replacement (HMR) in development - Uses `django-vite` for seamless Django integration -Development: +**Development with Docker (Recommended):** ```bash -docker-compose up # Starts Django + Vite dev server +docker-compose up # Starts Django + Vite dev server together ``` -Production: -- Assets are built during Docker image build -- Served via WhiteNoise from `/static/dist/` +**Development without Docker:** +```bash +# Terminal 1: Start Vite dev server +cd frontend +npm install +npm run dev + +# Terminal 2: Start Django +python manage.py runserver +``` + +**Building for Production:** +```bash +# Build optimized assets locally +cd frontend +npm run build # Outputs to static/dist/ + +# Or let Docker handle it +docker build -t myapp . # Multi-stage build includes npm run build +``` + +**Troubleshooting:** + +| Issue | Solution | +|-------|----------| +| Vite HMR not working in Docker | Ensure `VITE_DEV_SERVER_HOST=vite` is set for the web service | +| Styles not updating | Check Tailwind is scanning `../templates/**/*.html` | +| Assets 404 in production | Run `python manage.py collectstatic` after building | +| `django_vite` template errors | Ensure `DEBUG=False` uses built manifest, not dev server | **CDN (Simple, No Build Step)** From 4c714711c7ce6213d7120bfe8e84fc68a56aee8b Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Sun, 30 Nov 2025 23:49:36 +0530 Subject: [PATCH 7/8] refactor: improve Vite setup consistency - Use npm ci instead of npm install in docker-compose for deterministic installs - Remove redundant frontend/node_modules/ from .gitignore - Add settings assertions to Vite test for django_vite config --- template/.gitignore.jinja | 1 - template/docker-compose.yml.jinja | 2 +- tests/test_generation.py | 6 ++++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/template/.gitignore.jinja b/template/.gitignore.jinja index 975583c..4f75595 100644 --- a/template/.gitignore.jinja +++ b/template/.gitignore.jinja @@ -49,7 +49,6 @@ poetry.lock # Node node_modules/ -frontend/node_modules/ {% endif -%} # IDEs diff --git a/template/docker-compose.yml.jinja b/template/docker-compose.yml.jinja index dc3ba19..58e1c49 100644 --- a/template/docker-compose.yml.jinja +++ b/template/docker-compose.yml.jinja @@ -78,7 +78,7 @@ services: - ./static:/app/static ports: - "5173:5173" - command: sh -c "npm install && npm run dev" + command: sh -c "npm ci && npm run dev" {% endif %} {% if background_tasks in ['temporal', 'both'] %} diff --git a/tests/test_generation.py b/tests/test_generation.py index 0667ba6..2850e13 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -222,6 +222,12 @@ def test_htmx_frontend_templates_generated(generate): assert (frontend_dir / "vite.config.js").exists() assert (frontend_dir / "tailwind.config.js").exists() + # Check Django settings include django_vite + settings = project / "config" / "settings" / "base.py" + settings_content = settings.read_text() + assert "django_vite" in settings_content + assert "DJANGO_VITE" in settings_content + def test_htmx_frontend_cdn_mode(generate): """Test that HTMX frontend with CDN mode uses CDN links.""" From ac91dbabb62e68d5f88ff91c262c53ca86c6236d Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Mon, 1 Dec 2025 00:37:31 +0530 Subject: [PATCH 8/8] refactor: address review feedback - Explicitly test Vite in all-features test - Remove redundant mkdir in Dockerfile (Vite creates dirs) - Clarify troubleshooting doc for VITE_DEV_SERVER_HOST --- docs/features/frontend-options.md | 2 +- template/Dockerfile.jinja | 4 ++-- tests/test_features.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/features/frontend-options.md b/docs/features/frontend-options.md index 9492c1c..3739b13 100644 --- a/docs/features/frontend-options.md +++ b/docs/features/frontend-options.md @@ -63,7 +63,7 @@ docker build -t myapp . # Multi-stage build includes npm run build | Issue | Solution | |-------|----------| -| Vite HMR not working in Docker | Ensure `VITE_DEV_SERVER_HOST=vite` is set for the web service | +| Vite HMR not working in Docker | Ensure `VITE_DEV_SERVER_HOST=vite` is set in the Django web service environment | | Styles not updating | Check Tailwind is scanning `../templates/**/*.html` | | Assets 404 in production | Run `python manage.py collectstatic` after building | | `django_vite` template errors | Ensure `DEBUG=False` uses built manifest, not dev server | diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index 0abffb3..984f32f 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -14,8 +14,8 @@ RUN npm ci # Copy rest of frontend COPY frontend/ ./ -# Create output directory and build assets -RUN mkdir -p /app/static && npm run build +# Build assets (outputs to ../static/dist per vite.config.js) +RUN npm run build {% endif -%} FROM python:{{ python_version }}-slim as base diff --git a/tests/test_features.py b/tests/test_features.py index 09a2672..69f9cd1 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -388,6 +388,7 @@ def test_all_features_enabled_generates_successfully(generate): dependency_manager="uv", api_style="both", frontend="htmx-tailwind", + frontend_bundling="vite", # Explicitly test Vite (the default) background_tasks="celery", use_channels=True, auth_backend="both",