@django-stack/cli 0.0.1 → 0.2.0

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.
Files changed (79) hide show
  1. package/README.md +27 -3
  2. package/dist/bin.js +2282 -2
  3. package/dist/index.d.ts +252 -1
  4. package/dist/index.js +1 -4
  5. package/dist/stack-CTyqYWCV.js +800 -0
  6. package/package.json +15 -7
  7. package/templates/backend/.dockerignore +11 -0
  8. package/templates/backend/Dockerfile +45 -0
  9. package/templates/backend/config/__init__.py +0 -0
  10. package/templates/backend/config/asgi.py +7 -0
  11. package/templates/backend/config/settings.py +87 -0
  12. package/templates/backend/config/studio.py +20 -0
  13. package/templates/backend/config/urls.py +9 -0
  14. package/templates/backend/config/wsgi.py +7 -0
  15. package/templates/backend/conftest.py +7 -0
  16. package/templates/backend/content/__init__.py +0 -0
  17. package/templates/backend/content/admin.py +107 -0
  18. package/templates/backend/content/apps.py +6 -0
  19. package/templates/backend/content/blocks.py +51 -0
  20. package/templates/backend/content/migrations/0001_initial.py +418 -0
  21. package/templates/backend/content/migrations/__init__.py +0 -0
  22. package/templates/backend/content/models.py +234 -0
  23. package/templates/backend/content/seed.py +153 -0
  24. package/templates/backend/content/tests/__init__.py +0 -0
  25. package/templates/backend/content/tests/test_api.py +73 -0
  26. package/templates/backend/entrypoint.sh +27 -0
  27. package/templates/backend/manage.py +16 -0
  28. package/templates/backend/pyproject.toml +33 -0
  29. package/templates/backend/vendor/.gitkeep +0 -0
  30. package/templates/base/.github/workflows/deploy.yml +70 -0
  31. package/templates/base/AGENTS.md +79 -0
  32. package/templates/base/CLAUDE.md +1 -0
  33. package/templates/base/Caddyfile +51 -0
  34. package/templates/base/README.md +87 -0
  35. package/templates/base/SECURITY.md +12 -0
  36. package/templates/base/compose.yaml +282 -0
  37. package/templates/base/env.example +83 -0
  38. package/templates/base/gitignore +15 -0
  39. package/templates/base/package.json +21 -0
  40. package/templates/frontend-astro/.dockerignore +4 -0
  41. package/templates/frontend-astro/Dockerfile +32 -0
  42. package/templates/frontend-astro/astro.config.mjs +38 -0
  43. package/templates/frontend-astro/package.json +31 -0
  44. package/templates/frontend-astro/public/favicon.svg +1 -0
  45. package/templates/frontend-astro/src/components/Blocks.astro +32 -0
  46. package/templates/frontend-astro/src/components/ContactForm.astro +68 -0
  47. package/templates/frontend-astro/src/components/Image.astro +34 -0
  48. package/templates/frontend-astro/src/components/PostCard.astro +41 -0
  49. package/templates/frontend-astro/src/components/PostList.astro +39 -0
  50. package/templates/frontend-astro/src/components/RichText.astro +14 -0
  51. package/templates/frontend-astro/src/components/Seo.astro +20 -0
  52. package/templates/frontend-astro/src/components/blocks/CallToAction.astro +23 -0
  53. package/templates/frontend-astro/src/components/blocks/Image.astro +24 -0
  54. package/templates/frontend-astro/src/components/blocks/Quote.astro +10 -0
  55. package/templates/frontend-astro/src/components/blocks/Text.astro +8 -0
  56. package/templates/frontend-astro/src/layouts/Base.astro +122 -0
  57. package/templates/frontend-astro/src/lib/api.ts +24 -0
  58. package/templates/frontend-astro/src/lib/cache.ts +58 -0
  59. package/templates/frontend-astro/src/lib/headless.ts +344 -0
  60. package/templates/frontend-astro/src/middleware.ts +69 -0
  61. package/templates/frontend-astro/src/pages/404.astro +13 -0
  62. package/templates/frontend-astro/src/pages/[...slug].astro +21 -0
  63. package/templates/frontend-astro/src/pages/blog/[slug].astro +50 -0
  64. package/templates/frontend-astro/src/pages/blog/index.astro +15 -0
  65. package/templates/frontend-astro/src/pages/category/[slug].astro +25 -0
  66. package/templates/frontend-astro/src/pages/index.astro +28 -0
  67. package/templates/frontend-astro/src/pages/preview/exit.ts +7 -0
  68. package/templates/frontend-astro/src/pages/preview/index.ts +26 -0
  69. package/templates/frontend-astro/src/pages/revalidate.ts +19 -0
  70. package/templates/frontend-astro/src/pages/robots.txt.ts +7 -0
  71. package/templates/frontend-astro/src/pages/rss.xml.ts +19 -0
  72. package/templates/frontend-astro/src/pages/sitemap.xml.ts +29 -0
  73. package/templates/frontend-astro/src/pages/tag/[slug].astro +24 -0
  74. package/templates/frontend-astro/src/styles/global.css +101 -0
  75. package/templates/frontend-astro/test/blocks.test.ts +42 -0
  76. package/templates/frontend-astro/test/cache.test.ts +22 -0
  77. package/templates/frontend-astro/tsconfig.json +5 -0
  78. package/templates/frontend-astro/vendor/.gitkeep +0 -0
  79. package/templates/frontend-astro/vitest.config.ts +8 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@django-stack/cli",
3
- "version": "0.0.1",
4
- "description": "Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
3
+ "version": "0.2.0",
4
+ "description": "The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
5
5
  "license": "MIT",
6
6
  "author": "Leon van der Grient",
7
7
  "homepage": "https://github.com/dwarsbit/django-stack/tree/main/packages/cli#readme",
@@ -28,21 +28,29 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "files": [
30
30
  "dist",
31
+ "templates",
31
32
  "README.md"
32
33
  ],
33
- "sideEffects": false,
34
34
  "engines": {
35
35
  "node": ">=22"
36
36
  },
37
37
  "bin": {
38
- "django-stack": "./dist/bin.js"
38
+ "dstack": "./dist/bin.js"
39
39
  },
40
40
  "publishConfig": {
41
- "access": "public"
41
+ "access": "public",
42
+ "provenance": true
43
+ },
44
+ "dependencies": {
45
+ "@bomb.sh/tab": "^0.0.22",
46
+ "@clack/prompts": "^1.8.1",
47
+ "@napi-rs/keyring": "^2.1.2",
48
+ "commander": "^15.0.0",
49
+ "picocolors": "^1.1.1"
42
50
  },
43
51
  "scripts": {
44
- "build": "tsdown",
52
+ "build": "node scripts/copy-templates.mjs && tsdown",
45
53
  "typecheck": "tsc --noEmit",
46
- "test": "vitest run"
54
+ "test": "node scripts/copy-templates.mjs && vitest run"
47
55
  }
48
56
  }
@@ -0,0 +1,11 @@
1
+ .venv
2
+ __pycache__
3
+ *.pyc
4
+ .pytest_cache
5
+ .ruff_cache
6
+ media
7
+ staticfiles
8
+ db.sqlite3
9
+ .env
10
+ backups
11
+ huey-stats.db*
@@ -0,0 +1,45 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ # The backend image: Django, the API and the admin. The `dev` target runs
4
+ # the development server with the source mounted; `prod` runs Gunicorn.
5
+ FROM python:3.13-slim AS base
6
+ ENV PYTHONDONTWRITEBYTECODE=1 \
7
+ PYTHONUNBUFFERED=1 \
8
+ UV_COMPILE_BYTECODE=1 \
9
+ UV_LINK_MODE=copy \
10
+ UV_PROJECT_ENVIRONMENT=/venv \
11
+ PATH="/venv/bin:$PATH"
12
+ COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /bin/uv
13
+ # The Postgres client, for the backups (pg_dump and pg_restore).
14
+ RUN apt-get update \
15
+ && apt-get install --yes --no-install-recommends postgresql-client \
16
+ && rm -rf /var/lib/apt/lists/*
17
+ RUN groupadd --system app && useradd --system --gid app --home-dir /app app
18
+ WORKDIR /app
19
+
20
+ FROM base AS deps
21
+ COPY pyproject.toml uv.lock* ./
22
+ COPY vendor/ vendor/
23
+ RUN --mount=type=cache,target=/root/.cache/uv \
24
+ uv sync --no-dev --no-install-project
25
+
26
+ # Development runs as root, so files the container writes into the mounted
27
+ # source (such as new migrations) work on every host. Production does not.
28
+ FROM deps AS dev
29
+ RUN --mount=type=cache,target=/root/.cache/uv \
30
+ uv sync --no-install-project
31
+ ENTRYPOINT ["/app/entrypoint.sh"]
32
+ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
33
+
34
+ FROM base AS prod
35
+ COPY --from=deps /venv /venv
36
+ # The code is owned by root: the app user can read it, not change it.
37
+ COPY . .
38
+ RUN DJANGO_SECRET_KEY=collectstatic-only python manage.py collectstatic --noinput \
39
+ && mkdir -p /app/media /app/backups && chown -R app:app /app/media /app/backups
40
+ USER app
41
+ EXPOSE 8000
42
+ HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
43
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/readyz', timeout=4)"
44
+ ENTRYPOINT ["/app/entrypoint.sh"]
45
+ CMD ["gunicorn", "config.asgi:application", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-", "--no-control-socket"]
File without changes
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.asgi import get_asgi_application
4
+
5
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
+
7
+ application = get_asgi_application()
@@ -0,0 +1,87 @@
1
+ """
2
+ Settings of the {{project_name}} backend.
3
+
4
+ The environment-driven settings (secrets, database, cache, mail, hosts,
5
+ storage) and the defaults for Django Headless and Django Content Studio
6
+ come from ``django_stack.settings.configure``; see the variables in
7
+ ``.env.example`` at the project root. Settings below the call override it.
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ import django_stack.settings
13
+
14
+ BASE_DIR = Path(__file__).resolve().parent.parent
15
+
16
+ DJANGO_STACK = {
17
+ # Models anyone may create through the API, such as the contact form.
18
+ "PUBLIC_CREATE": ["content.formsubmission"],
19
+ # The apps export_content and import_content move between environments.
20
+ "CONTENT_APPS": ["content"],
21
+ # The function `manage.py seed_content` runs.
22
+ "SEED_CONTENT": "content.seed.seed",
23
+ }
24
+
25
+ django_stack.settings.configure(globals(), base_dir=BASE_DIR)
26
+
27
+ INSTALLED_APPS = [
28
+ "django.contrib.admin", # Content Studio records its activity log with it
29
+ "django.contrib.auth",
30
+ "django.contrib.contenttypes",
31
+ "django.contrib.sessions",
32
+ "django.contrib.messages",
33
+ "django.contrib.staticfiles",
34
+ "rest_framework",
35
+ "huey.contrib.djhuey",
36
+ "huey.contrib.djhuey.stats",
37
+ "headless",
38
+ "content_studio",
39
+ "content_studio.media_library",
40
+ "content_studio.contrib.password_reset",
41
+ "django_stack",
42
+ "content",
43
+ # Last: builds the API routes once every model is exposed.
44
+ "headless.rest",
45
+ ]
46
+
47
+ MIDDLEWARE = [
48
+ "django.middleware.security.SecurityMiddleware",
49
+ "whitenoise.middleware.WhiteNoiseMiddleware",
50
+ "django.contrib.sessions.middleware.SessionMiddleware",
51
+ "django.middleware.common.CommonMiddleware",
52
+ "django.middleware.csrf.CsrfViewMiddleware",
53
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
54
+ "django.contrib.messages.middleware.MessageMiddleware",
55
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
56
+ ]
57
+
58
+ ROOT_URLCONF = "config.urls"
59
+ WSGI_APPLICATION = "config.wsgi.application"
60
+ ASGI_APPLICATION = "config.asgi.application"
61
+
62
+ TEMPLATES = [
63
+ {
64
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
65
+ "DIRS": [],
66
+ "APP_DIRS": True,
67
+ "OPTIONS": {
68
+ "context_processors": [
69
+ "django.template.context_processors.request",
70
+ "django.contrib.auth.context_processors.auth",
71
+ "django.contrib.messages.context_processors.messages",
72
+ ],
73
+ },
74
+ },
75
+ ]
76
+
77
+ AUTH_PASSWORD_VALIDATORS = [
78
+ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
79
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
80
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
81
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
82
+ ]
83
+
84
+ LANGUAGE_CODE = "en-us"
85
+ TIME_ZONE = "UTC"
86
+ USE_I18N = True
87
+ USE_TZ = True
@@ -0,0 +1,20 @@
1
+ """
2
+ The Content Studio site of {{project_name}}: the admin at /admin/.
3
+ CONTENT_STUDIO["ADMIN_SITE"] points here. See
4
+ https://djangocontentstudio.org/docs/extensions for the hooks and extensions.
5
+ """
6
+
7
+ from content_studio.admin import ContentStudioSite
8
+ from content_studio.contrib.huey import HueyAdapter
9
+ from content_studio.dashboard.tasks import TaskDashboard
10
+ from content_studio.extensions import UserMenuLink
11
+
12
+
13
+ class SiteStudio(ContentStudioSite):
14
+ extensions = [
15
+ TaskDashboard(adapter=HueyAdapter()),
16
+ UserMenuLink(url="/tasks/", label="Tasks", icon="ph-bold ph-queue"),
17
+ ]
18
+
19
+
20
+ admin_site = SiteStudio()
@@ -0,0 +1,9 @@
1
+ from django.urls import include, path
2
+ from django_stack.urls import media_urlpatterns
3
+
4
+ urlpatterns = [
5
+ path("admin/", include("content_studio.urls")),
6
+ path("api/", include("headless.rest.urls")),
7
+ path("", include("django_stack.urls")), # /healthz and /readyz
8
+ *media_urlpatterns(), # uploaded files, in development only
9
+ ]
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.wsgi import get_wsgi_application
4
+
5
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
+
7
+ application = get_wsgi_application()
@@ -0,0 +1,7 @@
1
+ import pytest
2
+
3
+
4
+ @pytest.fixture(autouse=True)
5
+ def _no_revalidate(settings):
6
+ """Tests never call the frontend."""
7
+ settings.DJANGO_STACK = {**settings.DJANGO_STACK, "REVALIDATE_URL": None}
File without changes
@@ -0,0 +1,107 @@
1
+ """
2
+ The studios of the content models: how they look in Content Studio at
3
+ /admin/. See https://djangocontentstudio.org/docs/registering-models.
4
+ """
5
+
6
+ from content_studio import register
7
+ from content_studio.admin import ModelStudio
8
+ from content_studio.form import ActivityLog, Field, FormSet
9
+ from django_stack.studio import PreviewButton
10
+
11
+ from .models import Author, Category, FormSubmission, Menu, Page, Post, Redirect, SiteSettings, Tag
12
+
13
+ # Components on more than one studio need their own component_id.
14
+ PAGE_PREVIEW = PreviewButton(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a01")
15
+ POST_PREVIEW = PreviewButton(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a02")
16
+ PAGE_ACTIVITY = ActivityLog(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a03")
17
+ POST_ACTIVITY = ActivityLog(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a04")
18
+ SEO = FormSet(
19
+ title="Search and sharing",
20
+ description="Leave empty to use the title and the site settings.",
21
+ fields=["seo_title", "seo_description", "share_image"],
22
+ )
23
+
24
+
25
+ @register(Page)
26
+ class PageStudio(ModelStudio):
27
+ icon = "ph-bold ph-file-text"
28
+ table_display = ["title", "path", "status", "published_at"]
29
+ list_filter = ["status"]
30
+ search_fields = ["title", "path"]
31
+ ordering = ["path"]
32
+ prepopulated_fields = {"slug": ["title"]}
33
+ allow_duplication = True
34
+ edit_width = "wide"
35
+ edit_main = [
36
+ FormSet(title="Page", fields=["title", Field("slug", col_span=1), Field("parent", col_span=1)]),
37
+ FormSet(title="Body", fields=["body"]),
38
+ SEO,
39
+ ]
40
+ edit_sidebar = ["status", "published_at", PAGE_PREVIEW, "order", PAGE_ACTIVITY]
41
+
42
+
43
+ @register(Post)
44
+ class PostStudio(ModelStudio):
45
+ icon = "ph-bold ph-article"
46
+ table_display = ["title", "author", "status", "published_at"]
47
+ list_filter = ["status", "categories", "tags", "author"]
48
+ search_fields = ["title", "excerpt"]
49
+ ordering = ["-published_at"]
50
+ prepopulated_fields = {"slug": ["title"]}
51
+ allow_duplication = True
52
+ edit_width = "wide"
53
+ edit_main = [
54
+ FormSet(title="Post", fields=["title", "slug", "cover", "excerpt"]),
55
+ FormSet(title="Body", fields=["body"]),
56
+ SEO,
57
+ ]
58
+ edit_sidebar = ["status", "published_at", POST_PREVIEW, "author", "categories", "tags", POST_ACTIVITY]
59
+
60
+
61
+ @register(Category, Tag)
62
+ class TaxonomyStudio(ModelStudio):
63
+ icon = "ph-bold ph-tag"
64
+ table_display = ["name", "slug"]
65
+ search_fields = ["name"]
66
+ prepopulated_fields = {"slug": ["name"]}
67
+
68
+
69
+ @register(Author)
70
+ class AuthorStudio(ModelStudio):
71
+ icon = "ph-bold ph-user-circle"
72
+ table_display = ["name", "slug"]
73
+ search_fields = ["name"]
74
+ prepopulated_fields = {"slug": ["name"]}
75
+
76
+
77
+ @register(Menu)
78
+ class MenuStudio(ModelStudio):
79
+ icon = "ph-bold ph-list"
80
+ table_display = ["name", "slug"]
81
+ prepopulated_fields = {"slug": ["name"]}
82
+
83
+
84
+ @register(SiteSettings)
85
+ class SiteSettingsStudio(ModelStudio):
86
+ icon = "ph-bold ph-gear"
87
+ is_singleton = True
88
+ edit_main = [
89
+ FormSet(title="Site", fields=["site_name", "tagline", "logo", "home_page"]),
90
+ FormSet(title="Search and sharing", fields=["seo_description", "share_image"]),
91
+ FormSet(title="Footer", fields=["footer_text", "social_links"]),
92
+ ]
93
+
94
+
95
+ @register(Redirect)
96
+ class RedirectStudio(ModelStudio):
97
+ icon = "ph-bold ph-arrow-bend-up-right"
98
+ table_display = ["old_path", "new_path", "permanent"]
99
+ search_fields = ["old_path", "new_path"]
100
+
101
+
102
+ @register(FormSubmission)
103
+ class FormSubmissionStudio(ModelStudio):
104
+ icon = "ph-bold ph-envelope"
105
+ table_display = ["name", "email", "created_at"]
106
+ search_fields = ["name", "email", "message"]
107
+ ordering = ["-created_at"]
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ContentConfig(AppConfig):
5
+ name = "content"
6
+ verbose_name = "Content"
@@ -0,0 +1,51 @@
1
+ """
2
+ The blocks editors build page and post bodies from. Add a block here, add
3
+ it to a BlockField's ``blocks`` and render it in the frontend's block
4
+ registry (``frontend/src/components/blocks``). Blocks need no migration.
5
+ """
6
+
7
+ from content_studio.blocks import Block
8
+ from content_studio.fields import HTMLField
9
+ from content_studio.media_library.fields import MediaField
10
+ from django.db import models
11
+
12
+
13
+ class TextBlock(Block):
14
+ body = HTMLField()
15
+
16
+ class Meta:
17
+ description = "Rich text with headings, lists and links"
18
+ icon = "ph-bold ph-text-t"
19
+
20
+
21
+ class ImageBlock(Block):
22
+ image = MediaField(file_type="image", blank=False)
23
+ caption = models.CharField(max_length=300, blank=True)
24
+
25
+ class Meta:
26
+ description = "An image from the media library"
27
+ icon = "ph-bold ph-image"
28
+
29
+
30
+ class QuoteBlock(Block):
31
+ text = models.TextField()
32
+ attribution = models.CharField(max_length=200, blank=True)
33
+
34
+ class Meta:
35
+ description = "A quotation"
36
+ icon = "ph-bold ph-quotes"
37
+
38
+
39
+ class CallToActionBlock(Block):
40
+ title = models.CharField(max_length=200)
41
+ text = models.TextField(blank=True)
42
+ button_label = models.CharField(max_length=60)
43
+ button_url = models.CharField(max_length=500, help_text="A path on the site, such as /contact, or a full URL.")
44
+
45
+ class Meta:
46
+ name = "call_to_action"
47
+ description = "A highlighted message with a button"
48
+ icon = "ph-bold ph-megaphone"
49
+
50
+
51
+ BODY_BLOCKS = [TextBlock, ImageBlock, QuoteBlock, CallToActionBlock]