djangx 1.5.6__tar.gz → 1.5.7__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.
Files changed (83) hide show
  1. {djangx-1.5.6 → djangx-1.5.7}/PKG-INFO +1 -1
  2. {djangx-1.5.6 → djangx-1.5.7}/pyproject.toml +1 -1
  3. djangx-1.5.7/src/djangx/api/enums.py +8 -0
  4. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/settings/databases.py +9 -8
  5. djangx-1.5.7/src/djangx/enums.py +2 -0
  6. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/cli.py +14 -1
  7. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/generate.py +1 -11
  8. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/startproject.py +209 -71
  9. djangx-1.5.7/src/djangx/management/enums.py +63 -0
  10. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/settings/apps.py +39 -82
  11. {djangx-1.5.6 → djangx-1.5.7}/LICENSE +0 -0
  12. {djangx-1.5.6 → djangx-1.5.7}/README.md +0 -0
  13. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/__init__.py +0 -0
  14. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/__init__.py +0 -0
  15. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/backends/__init__.py +0 -0
  16. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/backends/auth.py +0 -0
  17. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/backends/server/__init__.py +0 -0
  18. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/backends/server/asgi.py +0 -0
  19. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/backends/server/wsgi.py +0 -0
  20. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/backends/storages.py +0 -0
  21. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/settings/__init__.py +0 -0
  22. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/settings/auth.py +0 -0
  23. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/settings/server.py +0 -0
  24. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/settings/storages.py +0 -0
  25. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/types/__init__.py +0 -0
  26. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/types/auth.py +0 -0
  27. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/types/databases.py +0 -0
  28. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/types/storages.py +0 -0
  29. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/api/urls.py +0 -0
  30. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/__init__.py +0 -0
  31. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/__init__.py +0 -0
  32. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/collectstatic.py +0 -0
  33. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/helpers/__init__.py +0 -0
  34. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/helpers/art.py +0 -0
  35. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/helpers/run.py +0 -0
  36. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/runbuild.py +0 -0
  37. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/runinstall.py +0 -0
  38. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/runserver.py +0 -0
  39. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/commands/tailwind.py +0 -0
  40. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/settings/__init__.py +0 -0
  41. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/settings/runcommands.py +0 -0
  42. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/settings/security.py +0 -0
  43. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/settings/tailwind.py +0 -0
  44. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/types/__init__.py +0 -0
  45. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/types/apps.py +0 -0
  46. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/management/urls.py +0 -0
  47. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/py.typed +0 -0
  48. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/settings.py +0 -0
  49. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/types.py +0 -0
  50. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/__init__.py +0 -0
  51. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/admin.py +0 -0
  52. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/settings/__init__.py +0 -0
  53. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/settings/contactinfo.py +0 -0
  54. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/settings/org.py +0 -0
  55. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/settings/social.py +0 -0
  56. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/css/.gitignore +0 -0
  57. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/css/aos.css +0 -0
  58. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/css/bootstrap-icons.min.css +0 -0
  59. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/css/fonts/bootstrap-icons.woff +0 -0
  60. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/css/fonts/bootstrap-icons.woff2 +0 -0
  61. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/img/apple-touch-icon.png +0 -0
  62. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/img/favicon.ico +0 -0
  63. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/img/logo.png +0 -0
  64. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/js/aos-init.js +0 -0
  65. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/js/aos.js +0 -0
  66. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/js/preloader.js +0 -0
  67. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/static/ui/js/scroll-top.js +0 -0
  68. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templates/ui/footer.html +0 -0
  69. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templates/ui/header.html +0 -0
  70. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templates/ui/hero.html +0 -0
  71. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templates/ui/index.html +0 -0
  72. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templates/ui/preloader.html +0 -0
  73. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templates/ui/scroll-top.html +0 -0
  74. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templatetags/__init__.py +0 -0
  75. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templatetags/contactinfo.py +0 -0
  76. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templatetags/org.py +0 -0
  77. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templatetags/social.py +0 -0
  78. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/templatetags/tailwind_css.py +0 -0
  79. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/types/__init__.py +0 -0
  80. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/types/contactinfo.py +0 -0
  81. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/types/org.py +0 -0
  82. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/types/social.py +0 -0
  83. {djangx-1.5.6 → djangx-1.5.7}/src/djangx/ui/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: djangx
3
- Version: 1.5.6
3
+ Version: 1.5.7
4
4
  Summary: Build and deploy Django apps with confidence.
5
5
  Author: Kevin Wasike Wakhisi
6
6
  Author-email: Kevin Wasike Wakhisi <kevin@christianwhocodes.space>
@@ -13,7 +13,7 @@ djx = "djangx.management.cli:main"
13
13
 
14
14
  [project]
15
15
  name = "djangx"
16
- version = "1.5.6"
16
+ version = "1.5.7"
17
17
  description = "Build and deploy Django apps with confidence."
18
18
  readme = "README.md"
19
19
  license = { file = "LICENSE" }
@@ -0,0 +1,8 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DatabaseBackend(StrEnum):
5
+ """Available database backends."""
6
+
7
+ SQLITE3 = "sqlite3"
8
+ POSTGRESQL = "postgresql"
@@ -5,6 +5,7 @@
5
5
  # https://www.postgresql.org/docs/current/libpq-pgpass.html
6
6
  # ==============================================================================
7
7
  from ... import PROJECT_DIR, Conf, ConfField
8
+ from ..enums import DatabaseBackend
8
9
  from ..types import DatabaseDict, DatabaseOptionsDict, DatabasesDict
9
10
 
10
11
 
@@ -13,10 +14,10 @@ class DatabaseConf(Conf):
13
14
 
14
15
  backend = ConfField(
15
16
  type=str,
16
- choices=["sqlite3", "postgresql"],
17
+ choices=[DatabaseBackend.SQLITE3, DatabaseBackend.POSTGRESQL],
17
18
  env="DB_BACKEND",
18
19
  toml="db.backend",
19
- default="sqlite3",
20
+ default=DatabaseBackend.SQLITE3,
20
21
  )
21
22
  # postgresql specific
22
23
  use_vars = ConfField(
@@ -79,14 +80,14 @@ def _get_databases_config() -> DatabasesDict:
79
80
  backend: str = _DATABASE.backend.lower()
80
81
 
81
82
  match backend:
82
- case "sqlite" | "sqlite3":
83
+ case DatabaseBackend.SQLITE3:
83
84
  return {
84
85
  "default": {
85
- "ENGINE": "django.db.backends.sqlite3",
86
- "NAME": PROJECT_DIR / "db.sqlite3",
86
+ "ENGINE": f"django.db.backends.{DatabaseBackend.SQLITE3.value}",
87
+ "NAME": PROJECT_DIR / f"db.{DatabaseBackend.SQLITE3.value}",
87
88
  }
88
89
  }
89
- case "postgresql" | "postgres" | "psql" | "pgsql" | "pg" | "psycopg":
90
+ case DatabaseBackend.POSTGRESQL:
90
91
  options: DatabaseOptionsDict = {
91
92
  "pool": _DATABASE.pool,
92
93
  "sslmode": _DATABASE.ssl_mode,
@@ -95,7 +96,7 @@ def _get_databases_config() -> DatabasesDict:
95
96
  # Add service or connection vars
96
97
  if _DATABASE.use_vars:
97
98
  config: DatabaseDict = {
98
- "ENGINE": "django.db.backends.postgresql",
99
+ "ENGINE": f"django.db.backends.{DatabaseBackend.POSTGRESQL.value}",
99
100
  "NAME": _DATABASE.name,
100
101
  "USER": _DATABASE.user,
101
102
  "PASSWORD": _DATABASE.password,
@@ -106,7 +107,7 @@ def _get_databases_config() -> DatabasesDict:
106
107
  else:
107
108
  options["service"] = _DATABASE.service
108
109
  config: DatabaseDict = {
109
- "ENGINE": "django.db.backends.postgresql",
110
+ "ENGINE": f"django.db.backends.{DatabaseBackend.POSTGRESQL.value}",
110
111
  "NAME": _DATABASE.name,
111
112
  "OPTIONS": options,
112
113
  }
@@ -0,0 +1,2 @@
1
+ from .api.enums import * # noqa: F403
2
+ from .management.enums import * # noqa: F403
@@ -17,6 +17,7 @@ def main() -> Optional[NoReturn]:
17
17
  case "startproject" | "init" | "new":
18
18
  from argparse import ArgumentParser, Namespace
19
19
 
20
+ from ..enums import DatabaseBackend
20
21
  from .commands.startproject import initialize
21
22
 
22
23
  parser = ArgumentParser(description=f"Initialize a new {PKG_DISPLAY_NAME} project")
@@ -25,9 +26,21 @@ def main() -> Optional[NoReturn]:
25
26
  choices=["default", "vercel"],
26
27
  help="Project preset to use (skips interactive prompt)",
27
28
  )
29
+ parser.add_argument(
30
+ "--database",
31
+ "--db",
32
+ choices=[DatabaseBackend.SQLITE3, DatabaseBackend.POSTGRESQL],
33
+ help=f"Database backend to use (skips interactive prompt). Note: Vercel preset requires {DatabaseBackend.POSTGRESQL.value}.",
34
+ )
35
+ parser.add_argument(
36
+ "--force",
37
+ "-f",
38
+ action="store_true",
39
+ help="Skip directory validation and initialize even if directory is not empty",
40
+ )
28
41
  args: Namespace = parser.parse_args(sys.argv[2:])
29
42
 
30
- sys.exit(initialize(preset=args.preset))
43
+ sys.exit(initialize(preset=args.preset, database=args.database, force=args.force))
31
44
 
32
45
  case _:
33
46
  from os import environ
@@ -1,11 +1,9 @@
1
1
  import builtins
2
2
  import pathlib
3
- from enum import StrEnum
4
3
  from typing import Any, Optional, cast
5
4
 
6
5
  from christianwhocodes.generators import (
7
6
  FileGenerator,
8
- FileGeneratorOption,
9
7
  PgPassFileGenerator,
10
8
  PgServiceFileGenerator,
11
9
  SSHConfigFileGenerator,
@@ -13,6 +11,7 @@ from christianwhocodes.generators import (
13
11
  from django.core.management.base import BaseCommand, CommandParser
14
12
 
15
13
  from ... import PKG_DISPLAY_NAME, PKG_NAME, PROJECT_API_DIR, PROJECT_DIR, Conf
14
+ from ..enums import FileOption
16
15
 
17
16
 
18
17
  class _ServerFileGenerator(FileGenerator):
@@ -222,15 +221,6 @@ class _EnvFileGenerator(FileGenerator):
222
221
  return str(value)
223
222
 
224
223
 
225
- class FileOption(StrEnum):
226
- PG_SERVICE = FileGeneratorOption.PG_SERVICE.value
227
- PGPASS = FileGeneratorOption.PGPASS.value
228
- SSH_CONFIG = FileGeneratorOption.SSH_CONFIG.value
229
- ENV = "env"
230
- SERVER = "server"
231
- VERCEL = "vercel"
232
-
233
-
234
224
  class Command(BaseCommand):
235
225
  help: str = "Generate configuration files (e.g., .env.example, vercel.json, asgi.py, wsgi.py, .pg_service.conf, pgpass.conf / .pgpass, ssh config)."
236
226
 
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from enum import StrEnum
3
2
  from pathlib import Path
4
3
  from typing import Final
5
4
 
@@ -9,6 +8,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
9
8
  from rich.prompt import Confirm, Prompt
10
9
 
11
10
  from ... import PKG_DISPLAY_NAME, PKG_NAME, PROJECT_DIR, PROJECT_INIT_NAME
11
+ from ...enums import DatabaseBackend, PresetType
12
12
 
13
13
  __all__ = ["initialize"]
14
14
 
@@ -29,29 +29,21 @@ SAFE_DIRECTORY_ITEMS: Final[set[str]] = {
29
29
  }
30
30
 
31
31
 
32
- class PresetType(StrEnum):
33
- """Available project presets."""
34
-
35
- DEFAULT = "default"
36
- VERCEL = "vercel"
37
-
38
-
39
32
  @dataclass(frozen=True)
40
33
  class _ProjectDependencies:
41
34
  """Manages project dependencies."""
42
35
 
43
- base: tuple[str, ...] = (
44
- "pillow>=12.1.0",
45
- "psycopg[binary,pool]>=3.3.2",
46
- )
36
+ base: tuple[str, ...] = ("pillow>=12.1.0",)
47
37
  dev: tuple[str, ...] = ("djlint>=1.36.4", "ruff>=0.15.0")
38
+ postgresql: tuple[str, ...] = ("psycopg[binary,pool]>=3.3.2",)
48
39
  vercel: tuple[str, ...] = ("vercel>=0.3.8",)
49
40
 
50
- def get_for_preset(self, preset: PresetType) -> list[str]:
51
- """Get dependencies for a specific preset.
41
+ def get_for_config(self, preset: PresetType, database: DatabaseBackend) -> list[str]:
42
+ """Get dependencies for a specific configuration.
52
43
 
53
44
  Args:
54
45
  preset: The preset type.
46
+ database: The database backend.
55
47
 
56
48
  Returns:
57
49
  List of dependency strings including base dependencies.
@@ -59,6 +51,11 @@ class _ProjectDependencies:
59
51
  deps = list(self.base)
60
52
  deps.append(f"{PKG_NAME}>={Version.get(PKG_NAME)[0]}")
61
53
 
54
+ # Add database-specific dependencies
55
+ if database == DatabaseBackend.POSTGRESQL:
56
+ deps.extend(self.postgresql)
57
+
58
+ # Add preset-specific dependencies
62
59
  if preset == PresetType.VERCEL:
63
60
  deps.extend(self.vercel)
64
61
 
@@ -71,7 +68,7 @@ class _TemplateManager:
71
68
  @staticmethod
72
69
  def gitignore() -> str:
73
70
  """Generate .gitignore content."""
74
- return """
71
+ return f"""
75
72
  # Python-generated files
76
73
  __pycache__/
77
74
  *.py[oc]
@@ -95,8 +92,8 @@ wheels/
95
92
  # Environment variables files
96
93
  /.env*
97
94
 
98
- # SQLite database file
99
- /db.sqlite3
95
+ # {DatabaseBackend.SQLITE3.value.capitalize()} database file
96
+ /db.{DatabaseBackend.SQLITE3.value}
100
97
  """.strip()
101
98
 
102
99
  @staticmethod
@@ -110,16 +107,23 @@ A new project built with {PKG_DISPLAY_NAME}.
110
107
  ## Getting Started
111
108
 
112
109
  1. Install dependencies: `uv sync`
113
- 2. Run development server: `djx runserver`
110
+ 2. Run development server: `uv run djx runserver`
114
111
  """.strip()
115
112
 
116
113
  @staticmethod
117
- def pyproject_toml(preset: PresetType, dependencies: list[str]) -> str:
114
+ def pyproject_toml(
115
+ preset: PresetType,
116
+ database: DatabaseBackend,
117
+ dependencies: list[str],
118
+ use_postgres_env_vars: bool = False,
119
+ ) -> str:
118
120
  """Generate pyproject.toml content.
119
121
 
120
122
  Args:
121
123
  preset: The preset type.
124
+ database: The database backend.
122
125
  dependencies: List of project dependencies.
126
+ use_postgres_env_vars: Whether to use env vars for PostgreSQL config.
123
127
 
124
128
  Returns:
125
129
  Formatted pyproject.toml content.
@@ -128,15 +132,25 @@ A new project built with {PKG_DISPLAY_NAME}.
128
132
  deps_formatted = ",\n ".join(f'"{dep}"' for dep in dependencies)
129
133
  dev_deps_formatted = ",\n ".join(f'"{dep}"' for dep in deps.dev)
130
134
 
131
- # Preset-specific tool configuration
135
+ # Build tool configuration based on preset and database
136
+ tool_config_parts: list[str] = []
137
+
138
+ # Database configuration
139
+ if database == DatabaseBackend.POSTGRESQL:
140
+ tool_config_parts.append(
141
+ f'db = {{ backend = "{DatabaseBackend.POSTGRESQL.value}", use-vars = {str(use_postgres_env_vars).lower()} }}'
142
+ )
143
+
144
+ # Storage configuration (Vercel-specific)
145
+ if preset == PresetType.VERCEL:
146
+ tool_config_parts.append(
147
+ 'storage = { backend = "vercel", blob-token = "keep-your-vercel-blob-token-secret-in-env" }'
148
+ )
149
+
150
+ # Format the tool config section
132
151
  tool_config = ""
133
- match preset:
134
- case PresetType.VERCEL:
135
- tool_config = (
136
- '\nstorage = { backend = "vercel", blob-token = "your-vercel-blob-token" }\n'
137
- )
138
- case _:
139
- pass
152
+ if tool_config_parts:
153
+ tool_config = "\n" + "\n".join(tool_config_parts) + "\n"
140
154
 
141
155
  return f"""[project]
142
156
  name = "{PROJECT_INIT_NAME}"
@@ -213,18 +227,18 @@ class HomeView(TemplateView):
213
227
  @staticmethod
214
228
  def tailwind_css() -> str:
215
229
  """Generate Tailwind CSS content."""
216
- return """@import "tailwindcss";
230
+ return f"""@import "tailwindcss";
217
231
 
218
232
  /* =============================================================================
219
233
  SOURCE FILES
220
234
  ============================================================================= */
221
- @source "../../../../.venv/**/djangx/ui/templates/ui/**/*.html";
235
+ @source "../../../../.venv/**/{PKG_NAME}/ui/templates/ui/**/*.html";
222
236
  @source "../../../templates/home/**/*.html";
223
237
 
224
238
  /* =============================================================================
225
239
  THEME CONFIGURATION
226
240
  ============================================================================= */
227
- @theme {
241
+ @theme {{
228
242
  /* ---------------------------------------------------------------------------
229
243
  TYPOGRAPHY
230
244
  --------------------------------------------------------------------------- */
@@ -263,70 +277,70 @@ class HomeView(TemplateView):
263
277
  --color-nav-dropdown-bg: #2e2e2e; /* Dropdown background */
264
278
  --color-nav-dropdown: #d9d9d9; /* Dropdown text color */
265
279
  --color-nav-dropdown-hover: #ff4d4f; /* Dropdown hover state */
266
- }
280
+ }}
267
281
 
268
282
  /* =============================================================================
269
283
  LIGHT THEME OVERRIDES
270
284
  ============================================================================= */
271
- @theme light {
285
+ @theme light {{
272
286
  --color-background: rgba(41, 41, 41, 0.8);
273
287
  --color-surface: #484848;
274
- }
288
+ }}
275
289
 
276
290
  /* =============================================================================
277
291
  DARK THEME OVERRIDES
278
292
  ============================================================================= */
279
- @theme dark {
293
+ @theme dark {{
280
294
  --color-background: #060606;
281
295
  --color-surface: #252525;
282
296
  --color-default: #ffffff;
283
297
  --color-heading: #ffffff;
284
- }
298
+ }}
285
299
 
286
300
  /* =============================================================================
287
301
  UTILITY CLASSES
288
302
  ============================================================================= */
289
- @layer utilities {
303
+ @layer utilities {{
290
304
  /* Full-width container */
291
- .container-full {
305
+ .container-full {{
292
306
  @apply mx-auto w-full px-8;
293
- }
307
+ }}
294
308
 
295
309
  /* Responsive container (Mobile→SM→MD→LG→XL→2XL: 100%→92%→83%→80%→75%→1400px max) */
296
- .container {
310
+ .container {{
297
311
  @apply mx-auto w-full px-8 sm:w-11/12 sm:px-4 md:w-5/6 lg:w-4/5 xl:w-3/4 xl:px-0 2xl:max-w-[1400px];
298
- }
299
- }
312
+ }}
313
+ }}
300
314
 
301
315
  /* =============================================================================
302
316
  BASE STYLES - Global element styling
303
317
  ============================================================================= */
304
- @layer base {
305
- :root {
318
+ @layer base {{
319
+ :root {{
306
320
  @apply scroll-smooth;
307
- }
321
+ }}
308
322
 
309
- body {
323
+ body {{
310
324
  @apply bg-background text-default font-default antialiased;
311
- }
325
+ }}
312
326
 
313
327
  h1,
314
328
  h2,
315
329
  h3,
316
330
  h4,
317
331
  h5,
318
- h6 {
332
+ h6 {{
319
333
  @apply text-heading font-heading text-balance;
320
- }
334
+ }}
321
335
 
322
- a {
336
+ a {{
323
337
  @apply text-accent no-underline transition-colors duration-200 ease-in-out;
324
- }
338
+ }}
325
339
 
326
- a:hover {
340
+ a:hover {{
327
341
  color: color-mix(in srgb, var(--color-accent), white 15%);
328
- }
329
- }
342
+ }}
343
+ }}
330
344
  """
331
345
 
332
346
 
@@ -646,20 +660,110 @@ class _ProjectInitializer:
646
660
  )
647
661
  return PresetType(choice)
648
662
 
649
- def _create_core_files(self, preset: PresetType) -> None:
663
+ def _get_postgres_config_choice(self, preset: PresetType) -> bool:
664
+ """Get whether to use environment variables for PostgreSQL.
665
+
666
+ Args:
667
+ preset: The preset type (Vercel always uses env vars).
668
+
669
+ Returns:
670
+ True if using environment variables, False for pg_service/pgpass files.
671
+ """
672
+ # Vercel requires environment variables (no filesystem access)
673
+ if preset == PresetType.VERCEL:
674
+ return True
675
+
676
+ # For default preset, let user choose
677
+ self.console.print("\n[bold]PostgreSQL Configuration Method:[/bold]")
678
+ self.console.print(" • [cyan]Environment variables[/cyan]: Store credentials in .env file")
679
+ self.console.print(
680
+ " • [cyan]PostgreSQL service files[/cyan]: Use pg_service.conf and .pgpass"
681
+ )
682
+
683
+ use_env_vars = Confirm.ask(
684
+ "Use environment variables for PostgreSQL configuration?",
685
+ default=True,
686
+ console=self.console,
687
+ )
688
+
689
+ return use_env_vars
690
+
691
+ def _get_database_choice(
692
+ self, preset: PresetType, database: str | None = None
693
+ ) -> tuple[DatabaseBackend, bool]:
694
+ """Get the user's database choice and PostgreSQL config method.
695
+
696
+ Args:
697
+ preset: The preset type (Vercel requires PostgreSQL).
698
+ database: Optional database to use without prompting.
699
+
700
+ Returns:
701
+ Tuple of (database backend, use_env_vars_for_postgres).
702
+
703
+ Raises:
704
+ ValueError: If database is invalid or incompatible with preset.
705
+ """
706
+ # Vercel preset requires PostgreSQL (no file system access on Vercel)
707
+ if preset == PresetType.VERCEL:
708
+ if database and database != DatabaseBackend.POSTGRESQL:
709
+ raise ValueError(f"Vercel preset requires PostgreSQL database (got: {database})")
710
+ return DatabaseBackend.POSTGRESQL, True # Always use env vars with Vercel
711
+
712
+ # If database explicitly provided, validate it
713
+ if database:
714
+ try:
715
+ db_backend = DatabaseBackend(database)
716
+ except ValueError:
717
+ valid_databases = [db.value for db in DatabaseBackend]
718
+ raise ValueError(
719
+ f"Invalid database '{database}'. Must be one of: {', '.join(valid_databases)}"
720
+ ) from None
721
+
722
+ # Ask about PostgreSQL config method if applicable
723
+ use_env_vars = (
724
+ self._get_postgres_config_choice(preset)
725
+ if db_backend == DatabaseBackend.POSTGRESQL
726
+ else False
727
+ )
728
+ return db_backend, use_env_vars
729
+
730
+ # Interactive prompt for default preset
731
+ choice = Prompt.ask(
732
+ "Choose a database",
733
+ choices=[db.value for db in DatabaseBackend],
734
+ default=DatabaseBackend.SQLITE3.value,
735
+ console=self.console,
736
+ )
737
+ db_backend = DatabaseBackend(choice)
738
+
739
+ # Ask about PostgreSQL config method if applicable
740
+ use_env_vars = (
741
+ self._get_postgres_config_choice(preset)
742
+ if db_backend == DatabaseBackend.POSTGRESQL
743
+ else False
744
+ )
745
+ return db_backend, use_env_vars
746
+
747
+ def _create_core_files(
748
+ self, preset: PresetType, database: DatabaseBackend, use_postgres_env_vars: bool
749
+ ) -> None:
650
750
  """Create core project configuration files.
651
751
 
652
752
  Args:
653
753
  preset: The preset type to use.
754
+ database: The database backend to use.
755
+ use_postgres_env_vars: Whether to use env vars for PostgreSQL config.
654
756
 
655
757
  Raises:
656
758
  IOError: If files cannot be created.
657
759
  """
658
- dependencies = self.dependencies.get_for_preset(preset)
760
+ dependencies = self.dependencies.get_for_config(preset, database)
659
761
 
660
762
  # Create files only if they don't exist
661
763
  files_to_create = {
662
- "pyproject.toml": self.templates.pyproject_toml(preset, dependencies),
764
+ "pyproject.toml": self.templates.pyproject_toml(
765
+ preset, database, dependencies, use_postgres_env_vars
766
+ ),
663
767
  ".gitignore": self.templates.gitignore(),
664
768
  "README.md": self.templates.readme(),
665
769
  }
@@ -749,11 +853,15 @@ class _ProjectInitializer:
749
853
  home_creator = _HomeAppCreator(self.project_dir, self.writer, self.templates, self.console)
750
854
  home_creator.create()
751
855
 
752
- def _show_next_steps(self, preset: PresetType) -> None:
856
+ def _show_next_steps(
857
+ self, preset: PresetType, database: DatabaseBackend, use_postgres_env_vars: bool
858
+ ) -> None:
753
859
  """Display next steps for the user after successful initialization.
754
860
 
755
861
  Args:
756
862
  preset: The preset that was used.
863
+ database: The database backend that was chosen.
864
+ use_postgres_env_vars: Whether using env vars for PostgreSQL.
757
865
  """
758
866
  from rich.panel import Panel
759
867
 
@@ -762,13 +870,28 @@ class _ProjectInitializer:
762
870
  "2. Copy [bold].env.example[/bold] to [bold].env[/bold] and configure",
763
871
  ]
764
872
 
873
+ step_num = 3
874
+
875
+ # Database-specific instructions
876
+ if database == DatabaseBackend.POSTGRESQL:
877
+ if use_postgres_env_vars:
878
+ next_steps.append(
879
+ f"{step_num}. Configure PostgreSQL connection in [bold].env[/bold]"
880
+ )
881
+ else:
882
+ next_steps.append(
883
+ f"{step_num}. Configure PostgreSQL connection using [bold]pg_service.conf[/bold] and [bold].pgpass[/bold] files"
884
+ )
885
+ step_num += 1
886
+
887
+ # Preset-specific instructions
765
888
  if preset == PresetType.VERCEL:
766
- next_steps.append(
767
- "3. Configure Vercel blob token in [bold]pyproject.toml[/bold] or [bold].env[/bold]"
768
- )
769
- next_steps.append("4. Run development server: [bold cyan]djx runserver[/bold cyan]")
770
- else:
771
- next_steps.append("3. Run development server: [bold cyan]djx runserver[/bold cyan]")
889
+ next_steps.append(f"{step_num}. Configure Vercel blob token in [bold].env[/bold]")
890
+ step_num += 1
891
+
892
+ next_steps.append(
893
+ f"{step_num}. Run development server: [bold cyan]uv run djx runserver[/bold cyan]"
894
+ )
772
895
 
773
896
  panel = Panel(
774
897
  "\n".join(next_steps),
@@ -780,11 +903,14 @@ class _ProjectInitializer:
780
903
  self.console.print("\n")
781
904
  self.console.print(panel)
782
905
 
783
- def create(self, preset: str | None = None, force: bool = False) -> ExitCode:
906
+ def create(
907
+ self, preset: str | None = None, database: str | None = None, force: bool = False
908
+ ) -> ExitCode:
784
909
  """Execute the full project initialization workflow.
785
910
 
786
911
  Args:
787
912
  preset: Optional preset to use without prompting.
913
+ database: Optional database backend to use without prompting.
788
914
  force: Skip directory validation.
789
915
 
790
916
  Returns:
@@ -797,6 +923,11 @@ class _ProjectInitializer:
797
923
  # Get and validate preset choice
798
924
  chosen_preset = self._get_preset_choice(preset)
799
925
 
926
+ # Get and validate database choice
927
+ chosen_database, use_postgres_env_vars = self._get_database_choice(
928
+ chosen_preset, database
929
+ )
930
+
800
931
  with Progress(
801
932
  SpinnerColumn(),
802
933
  TextColumn("[progress.description]{task.description}"),
@@ -805,7 +936,7 @@ class _ProjectInitializer:
805
936
  ) as progress:
806
937
  # Create core configuration files
807
938
  task = progress.add_task("Creating project files...", total=None)
808
- self._create_core_files(chosen_preset)
939
+ self._create_core_files(chosen_preset, chosen_database, use_postgres_env_vars)
809
940
  progress.update(task, completed=True)
810
941
 
811
942
  # Configure preset-specific files
@@ -818,7 +949,7 @@ class _ProjectInitializer:
818
949
  self._create_home_app()
819
950
  progress.update(task, completed=True)
820
951
 
821
- self._show_next_steps(chosen_preset)
952
+ self._show_next_steps(chosen_preset, chosen_database, use_postgres_env_vars)
822
953
  return ExitCode.SUCCESS
823
954
 
824
955
  except KeyboardInterrupt:
@@ -861,14 +992,19 @@ class _ProjectInitializer:
861
992
  # ============================================================================
862
993
 
863
994
 
864
- def initialize(preset: str | None = None, force: bool = False) -> ExitCode:
865
- """Main entry point for project initialization.
995
+ def initialize(
996
+ preset: str | None = None, database: str | None = None, force: bool = False
997
+ ) -> ExitCode:
998
+ f"""Main entry point for project initialization.
866
999
 
867
- Creates a new project with the specified preset configuration.
1000
+ Creates a new project with the specified preset and database configuration.
868
1001
 
869
1002
  Args:
870
1003
  preset: Optional preset to use without prompting.
871
1004
  Available presets: 'default', 'vercel'.
1005
+ database: Optional database backend to use without prompting.
1006
+ Available databases: '{DatabaseBackend.SQLITE3.value}', '{DatabaseBackend.POSTGRESQL.value}'.
1007
+ Note: Vercel preset requires PostgreSQL.
872
1008
  force: Skip directory validation and proceed even if directory is not empty.
873
1009
 
874
1010
  Returns:
@@ -876,7 +1012,9 @@ def initialize(preset: str | None = None, force: bool = False) -> ExitCode:
876
1012
  ExitCode.ERROR otherwise.
877
1013
 
878
1014
  Example:
879
- >>> initialize(preset="vercel")
1015
+ >>> initialize(preset="vercel") # Will auto-select {DatabaseBackend.POSTGRESQL.value} due to Vercel preset requirement
1016
+ >>> initialize(database="{DatabaseBackend.POSTGRESQL.value}") # Default preset with {DatabaseBackend.POSTGRESQL.value}
1017
+ >>> initialize(preset="default", database="{DatabaseBackend.SQLITE3.value}") # Default preset with {DatabaseBackend.SQLITE3.value}
880
1018
  >>> initialize(force=True)
881
1019
  """
882
- return _ProjectInitializer(PROJECT_DIR).create(preset=preset, force=force)
1020
+ return _ProjectInitializer(PROJECT_DIR).create(preset=preset, database=database, force=force)
@@ -0,0 +1,63 @@
1
+ from enum import StrEnum
2
+
3
+ from christianwhocodes.generators import FileGeneratorOption
4
+
5
+
6
+ class FileOption(StrEnum):
7
+ PG_SERVICE = FileGeneratorOption.PG_SERVICE.value
8
+ PGPASS = FileGeneratorOption.PGPASS.value
9
+ SSH_CONFIG = FileGeneratorOption.SSH_CONFIG.value
10
+ ENV = "env"
11
+ SERVER = "server"
12
+ VERCEL = "vercel"
13
+
14
+
15
+ class PresetType(StrEnum):
16
+ """Available project presets."""
17
+
18
+ DEFAULT = "default"
19
+ VERCEL = "vercel"
20
+
21
+
22
+ class Apps(StrEnum):
23
+ """Django applications enumeration."""
24
+
25
+ ADMIN = "django.contrib.admin"
26
+ AUTH = "django.contrib.auth"
27
+ CONTENTTYPES = "django.contrib.contenttypes"
28
+ SESSIONS = "django.contrib.sessions"
29
+ MESSAGES = "django.contrib.messages"
30
+ STATICFILES = "django.contrib.staticfiles"
31
+ HTTP_COMPRESSION = "django_http_compression"
32
+ MINIFY_HTML = "django_minify_html"
33
+ BROWSER_RELOAD = "django_browser_reload"
34
+ WATCHFILES = "django_watchfiles"
35
+
36
+
37
+ class ContextProcessors(StrEnum):
38
+ """Django template context processors enumeration."""
39
+
40
+ DEBUG = "django.template.context_processors.debug"
41
+ REQUEST = "django.template.context_processors.request"
42
+ AUTH = "django.contrib.auth.context_processors.auth"
43
+ MESSAGES = "django.contrib.messages.context_processors.messages"
44
+ CSP = "django.template.context_processors.csp"
45
+
46
+
47
+ class Middlewares(StrEnum):
48
+ """Django middleware enumeration."""
49
+
50
+ SECURITY = "django.middleware.security.SecurityMiddleware"
51
+ SESSION = "django.contrib.sessions.middleware.SessionMiddleware"
52
+ COMMON = "django.middleware.common.CommonMiddleware"
53
+ CSRF = "django.middleware.csrf.CsrfViewMiddleware"
54
+ AUTH = "django.contrib.auth.middleware.AuthenticationMiddleware"
55
+ MESSAGES = "django.contrib.messages.middleware.MessageMiddleware"
56
+ CLICKJACKING = "django.middleware.clickjacking.XFrameOptionsMiddleware"
57
+ CSP = "django.middleware.csp.ContentSecurityPolicyMiddleware"
58
+ HTTP_COMPRESSION = "django_http_compression.middleware.HttpCompressionMiddleware"
59
+ MINIFY_HTML = "django_minify_html.middleware.MinifyHtmlMiddleware"
60
+ BROWSER_RELOAD = "django_browser_reload.middleware.BrowserReloadMiddleware"
61
+
62
+
63
+ __all__: list[str] = ["FileOption", "PresetType", "Apps", "ContextProcessors", "Middlewares"]
@@ -1,8 +1,3 @@
1
- """# DjangX settings: Installed apps, middleware, templates, context processors."""
2
- # TODO: Refactor so that the user can specify the order of apps, middleware, and context processors more flexibly.
3
-
4
- from enum import StrEnum
5
-
6
1
  from ... import (
7
2
  INCLUDE_PROJECT_MAIN_APP,
8
3
  PKG_API_NAME,
@@ -13,28 +8,16 @@ from ... import (
13
8
  Conf,
14
9
  ConfField,
15
10
  )
11
+ from ..enums import Apps, ContextProcessors, Middlewares
16
12
  from ..types import TemplatesDict
17
13
 
14
+ # TODO: Refactor so that the user can specify the order of apps, middleware, and context processors more flexibly.
15
+
18
16
  # ===============================================================
19
17
  # Apps
20
18
  # ===============================================================
21
19
 
22
20
 
23
- class _Apps(StrEnum):
24
- """Django applications enumeration."""
25
-
26
- ADMIN = "django.contrib.admin"
27
- AUTH = "django.contrib.auth"
28
- CONTENTTYPES = "django.contrib.contenttypes"
29
- SESSIONS = "django.contrib.sessions"
30
- MESSAGES = "django.contrib.messages"
31
- STATICFILES = "django.contrib.staticfiles"
32
- HTTP_COMPRESSION = "django_http_compression"
33
- MINIFY_HTML = "django_minify_html"
34
- BROWSER_RELOAD = "django_browser_reload"
35
- WATCHFILES = "django_watchfiles"
36
-
37
-
38
21
  class AppsConf(Conf):
39
22
  """Apps configuration settings."""
40
23
 
@@ -70,19 +53,19 @@ def _get_installed_apps() -> list[str]:
70
53
  ]
71
54
 
72
55
  third_party_apps: list[str] = [
73
- _Apps.HTTP_COMPRESSION,
74
- _Apps.MINIFY_HTML,
75
- _Apps.BROWSER_RELOAD,
76
- _Apps.WATCHFILES,
56
+ Apps.HTTP_COMPRESSION,
57
+ Apps.MINIFY_HTML,
58
+ Apps.BROWSER_RELOAD,
59
+ Apps.WATCHFILES,
77
60
  ]
78
61
 
79
62
  django_apps: list[str] = [
80
- _Apps.ADMIN,
81
- _Apps.AUTH,
82
- _Apps.CONTENTTYPES,
83
- _Apps.SESSIONS,
84
- _Apps.MESSAGES,
85
- _Apps.STATICFILES,
63
+ Apps.ADMIN,
64
+ Apps.AUTH,
65
+ Apps.CONTENTTYPES,
66
+ Apps.SESSIONS,
67
+ Apps.MESSAGES,
68
+ Apps.STATICFILES,
86
69
  ]
87
70
 
88
71
  # Collect apps that should be removed except for base apps
@@ -107,19 +90,9 @@ INSTALLED_APPS: list[str] = _get_installed_apps()
107
90
  # ===============================================================
108
91
 
109
92
 
110
- class _ContextProcessors(StrEnum):
111
- """Django template context processors enumeration."""
112
-
113
- DEBUG = "django.template.context_processors.debug"
114
- REQUEST = "django.template.context_processors.request"
115
- AUTH = "django.contrib.auth.context_processors.auth"
116
- MESSAGES = "django.contrib.messages.context_processors.messages"
117
- CSP = "django.template.context_processors.csp"
118
-
119
-
120
- _APP_CONTEXT_PROCESSOR_MAP: dict[_Apps, list[_ContextProcessors]] = {
121
- _Apps.AUTH: [_ContextProcessors.AUTH],
122
- _Apps.MESSAGES: [_ContextProcessors.MESSAGES],
93
+ _APP_CONTEXT_PROCESSOR_MAP: dict[Apps, list[ContextProcessors]] = {
94
+ Apps.AUTH: [ContextProcessors.AUTH],
95
+ Apps.MESSAGES: [ContextProcessors.MESSAGES],
123
96
  }
124
97
 
125
98
 
@@ -151,11 +124,11 @@ def _get_context_processors(installed_apps: list[str]) -> list[str]:
151
124
  """
152
125
  # Django context processors in recommended order
153
126
  django_context_processors: list[str] = [
154
- _ContextProcessors.DEBUG, # Debug info (only in DEBUG mode)
155
- _ContextProcessors.REQUEST, # Adds request object to context
156
- _ContextProcessors.AUTH, # Adds user and perms to context
157
- _ContextProcessors.MESSAGES, # Adds messages to context
158
- _ContextProcessors.CSP, # Content Security Policy
127
+ ContextProcessors.DEBUG, # Debug info (only in DEBUG mode)
128
+ ContextProcessors.REQUEST, # Adds request object to context
129
+ ContextProcessors.AUTH, # Adds user and perms to context
130
+ ContextProcessors.MESSAGES, # Adds messages to context
131
+ ContextProcessors.CSP, # Content Security Policy
159
132
  ]
160
133
 
161
134
  # Collect context processors that should be removed based on missing apps
@@ -193,29 +166,13 @@ TEMPLATES: TemplatesDict = [
193
166
  # ===============================================================
194
167
 
195
168
 
196
- class _Middlewares(StrEnum):
197
- """Django middleware enumeration."""
198
-
199
- SECURITY = "django.middleware.security.SecurityMiddleware"
200
- SESSION = "django.contrib.sessions.middleware.SessionMiddleware"
201
- COMMON = "django.middleware.common.CommonMiddleware"
202
- CSRF = "django.middleware.csrf.CsrfViewMiddleware"
203
- AUTH = "django.contrib.auth.middleware.AuthenticationMiddleware"
204
- MESSAGES = "django.contrib.messages.middleware.MessageMiddleware"
205
- CLICKJACKING = "django.middleware.clickjacking.XFrameOptionsMiddleware"
206
- CSP = "django.middleware.csp.ContentSecurityPolicyMiddleware"
207
- HTTP_COMPRESSION = "django_http_compression.middleware.HttpCompressionMiddleware"
208
- MINIFY_HTML = "django_minify_html.middleware.MinifyHtmlMiddleware"
209
- BROWSER_RELOAD = "django_browser_reload.middleware.BrowserReloadMiddleware"
210
-
211
-
212
- _APP_MIDDLEWARE_MAP: dict[_Apps, list[_Middlewares]] = {
213
- _Apps.SESSIONS: [_Middlewares.SESSION],
214
- _Apps.AUTH: [_Middlewares.AUTH],
215
- _Apps.MESSAGES: [_Middlewares.MESSAGES],
216
- _Apps.HTTP_COMPRESSION: [_Middlewares.HTTP_COMPRESSION],
217
- _Apps.MINIFY_HTML: [_Middlewares.MINIFY_HTML],
218
- _Apps.BROWSER_RELOAD: [_Middlewares.BROWSER_RELOAD],
169
+ _APP_MIDDLEWARE_MAP: dict[Apps, list[Middlewares]] = {
170
+ Apps.SESSIONS: [Middlewares.SESSION],
171
+ Apps.AUTH: [Middlewares.AUTH],
172
+ Apps.MESSAGES: [Middlewares.MESSAGES],
173
+ Apps.HTTP_COMPRESSION: [Middlewares.HTTP_COMPRESSION],
174
+ Apps.MINIFY_HTML: [Middlewares.MINIFY_HTML],
175
+ Apps.BROWSER_RELOAD: [Middlewares.BROWSER_RELOAD],
219
176
  }
220
177
 
221
178
 
@@ -260,17 +217,17 @@ def _get_middleware(installed_apps: list[str]) -> list[str]:
260
217
  - ABOVE any middleware that modifies HTML (like BrowserReloadMiddleware)
261
218
  """
262
219
  django_middleware: list[str] = [
263
- _Middlewares.SECURITY, # FIRST - security headers, HTTPS redirect
264
- _Middlewares.SESSION, # Early - needed by auth & messages
265
- _Middlewares.COMMON, # Early - URL normalization
266
- _Middlewares.CSRF, # After session - needs session data
267
- _Middlewares.AUTH, # After session - stores user in session
268
- _Middlewares.MESSAGES, # After session & auth
269
- _Middlewares.CLICKJACKING, # Security headers (X-Frame-Options)
270
- _Middlewares.CSP, # Security headers (Content-Security-Policy)
271
- _Middlewares.HTTP_COMPRESSION, # Before minify - encodes responses (Zstandard, Brotli, Gzip)
272
- _Middlewares.MINIFY_HTML, # After compression, before HTML modifiers
273
- _Middlewares.BROWSER_RELOAD, # LAST - dev only, injects reload script into HTML
220
+ Middlewares.SECURITY, # FIRST - security headers, HTTPS redirect
221
+ Middlewares.SESSION, # Early - needed by auth & messages
222
+ Middlewares.COMMON, # Early - URL normalization
223
+ Middlewares.CSRF, # After session - needs session data
224
+ Middlewares.AUTH, # After session - stores user in session
225
+ Middlewares.MESSAGES, # After session & auth
226
+ Middlewares.CLICKJACKING, # Security headers (X-Frame-Options)
227
+ Middlewares.CSP, # Security headers (Content-Security-Policy)
228
+ Middlewares.HTTP_COMPRESSION, # Before minify - encodes responses (Zstandard, Brotli, Gzip)
229
+ Middlewares.MINIFY_HTML, # After compression, before HTML modifiers
230
+ Middlewares.BROWSER_RELOAD, # LAST - dev only, injects reload script into HTML
274
231
  ]
275
232
 
276
233
  # Collect middleware that should be removed based on missing apps
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes