nextpy-framework 2.4.6__tar.gz → 2.4.8__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 (81) hide show
  1. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/cli.py +9 -10
  2. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/db.py +1 -1
  3. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx_preprocessor.py +35 -51
  4. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/app.py +91 -14
  5. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/debug.py +7 -6
  6. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8/.nextpy_framework/nextpy_framework.egg-info}/PKG-INFO +24 -9
  7. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/SOURCES.txt +5 -1
  8. {nextpy_framework-2.4.6/.nextpy_framework/nextpy_framework.egg-info → nextpy_framework-2.4.8}/PKG-INFO +24 -9
  9. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/README.md +23 -8
  10. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/pyproject.toml +1 -1
  11. nextpy_framework-2.4.8/tests/test_jsx_edgecases.py +32 -0
  12. nextpy_framework-2.4.8/tests/test_jsx_preprocessor.py +66 -0
  13. nextpy_framework-2.4.8/tests/test_server_features.py +47 -0
  14. nextpy_framework-2.4.8/tests/test_tailwind_up_to_date.py +22 -0
  15. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/__init__.py +0 -0
  16. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/auth.py +0 -0
  17. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/builder.py +0 -0
  18. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/__init__.py +0 -0
  19. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/debug/AutoDebug.py +0 -0
  20. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/debug/DebugIcon.py +0 -0
  21. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/debug/DebugIconFixed.py +0 -0
  22. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/feedback.py +0 -0
  23. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/form.py +0 -0
  24. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/head.py +0 -0
  25. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/hooks_provider.py +0 -0
  26. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/image.py +0 -0
  27. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/layout.py +0 -0
  28. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/link.py +0 -0
  29. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/loader.py +0 -0
  30. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/navigation.py +0 -0
  31. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/toast.py +0 -0
  32. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/ui.py +0 -0
  33. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/visual.py +0 -0
  34. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components.py +0 -0
  35. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/config.py +0 -0
  36. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/__init__.py +0 -0
  37. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/builder.py +0 -0
  38. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/component_renderer.py +0 -0
  39. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/component_router.py +0 -0
  40. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/data_fetching.py +0 -0
  41. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/demo_pages_simple.py +0 -0
  42. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/demo_router.py +0 -0
  43. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/renderer.py +0 -0
  44. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/router.py +0 -0
  45. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/sync.py +0 -0
  46. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/dev_server.py +0 -0
  47. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/dev_tools.py +0 -0
  48. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/errors.py +0 -0
  49. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks.py +0 -0
  50. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks_provider.py +0 -0
  51. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks_provider_new.py +0 -0
  52. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx.py +0 -0
  53. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx_transformer.py +0 -0
  54. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/main.py +0 -0
  55. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/performance.py +0 -0
  56. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/__init__.py +0 -0
  57. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/base.py +0 -0
  58. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/builtin.py +0 -0
  59. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/config.py +0 -0
  60. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins.py +0 -0
  61. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/py.typed +0 -0
  62. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/security.py +0 -0
  63. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/__init__.py +0 -0
  64. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/middleware.py +0 -0
  65. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/true_jsx.py +0 -0
  66. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/__init__.py +0 -0
  67. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/cache.py +0 -0
  68. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/email.py +0 -0
  69. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/file_upload.py +0 -0
  70. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/logging.py +0 -0
  71. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/search.py +0 -0
  72. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/seo.py +0 -0
  73. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/validators.py +0 -0
  74. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/websocket.py +0 -0
  75. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/dependency_links.txt +0 -0
  76. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/entry_points.txt +0 -0
  77. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/requires.txt +0 -0
  78. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/top_level.txt +0 -0
  79. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/LICENSE +0 -0
  80. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/setup.cfg +0 -0
  81. {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/tests/test_routing.py +0 -0
@@ -533,7 +533,7 @@ def version():
533
533
  click.echo(click.style("\n 📋 NextPy Framework Info", fg="cyan", bold=True))
534
534
  click.echo(click.style(" ===================\n", fg="cyan"))
535
535
 
536
- click.echo(f" 🏷️ Version: 2.4.6 ")
536
+ click.echo(f" 🏷️ Version: 2.4.8 ")
537
537
  click.echo(f" 🐍 Python: {sys.version.split()[0]}")
538
538
  click.echo(f" ⚡ Framework: NextPy")
539
539
  click.echo(f" 🎨 Architecture: True JSX")
@@ -885,7 +885,8 @@ def _create_project_structure(project_dir: Path):
885
885
  <meta charset="utf-8">
886
886
  <meta name="viewport" content="width=device-width, initial-scale=1">
887
887
  <title>{{ title or "NextPy App" }}</title>
888
- <script src="https://cdn.tailwindcss.com"></script>
888
+ <!-- reference compiled Tailwind CSS rather than CDN for better integration -->
889
+ <link href="/tailwind.css" rel="stylesheet">
889
890
  </head>
890
891
  <body>
891
892
  <div id="app">
@@ -901,13 +902,13 @@ def _create_project_structure(project_dir: Path):
901
902
  <meta charset="utf-8">
902
903
  <meta name="viewport" content="width=device-width, initial-scale=1">
903
904
  <title>404 - Page Not Found</title>
904
- <script src="https://cdn.tailwindcss.com"></script>
905
+ <link href="/tailwind.css" rel="stylesheet">
905
906
  </head>
906
- <body class="min-h-screen bg-gray-100 flex items-center justify-center">
907
+ <body class="flex items-center justify-center min-h-screen bg-gray-100">
907
908
  <div class="text-center">
908
- <h1 class="text-6xl font-bold text-gray-900 mb-4">404</h1>
909
- <p class="text-xl text-gray-600 mb-8">Page not found</p>
910
- <a href="/" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
909
+ <h1 class="mb-4 text-6xl font-bold text-gray-900">404</h1>
910
+ <p class="mb-8 text-xl text-gray-600">Page not found</p>
911
+ <a href="/" class="px-6 py-3 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
911
912
  Go Home
912
913
  </a>
913
914
  </div>
@@ -1010,9 +1011,7 @@ body {
1010
1011
  }
1011
1012
  },
1012
1013
  },
1013
- plugins: [
1014
- require('@tailwindcss/postcss'),
1015
- ],
1014
+ plugins: [],
1016
1015
  };''')
1017
1016
  click.echo(" Created: tailwind.config.js")
1018
1017
 
@@ -7,7 +7,7 @@ import os
7
7
  from typing import Optional, Dict, Any
8
8
  from sqlalchemy import create_engine
9
9
  from sqlalchemy.orm import sessionmaker, Session
10
- from sqlalchemy.ext.declarative import declarative_base
10
+ from sqlalchemy.orm import declarative_base
11
11
 
12
12
  Base = declarative_base()
13
13
 
@@ -74,9 +74,6 @@ class JSXPreprocessor:
74
74
  def preprocess_content(self, content: str, file_path: str = None) -> str:
75
75
  """Preprocess content containing JSX with enhanced error handling and plugin support"""
76
76
  try:
77
- # Validate basic JSX structure
78
- self._validate_jsx_structure(content, file_path)
79
-
80
77
  # Apply plugins if available
81
78
  if PLUGINS_AVAILABLE and plugin_manager:
82
79
  plugin_context = PluginContext(
@@ -247,55 +244,42 @@ class JSXPreprocessor:
247
244
  file_path=file_path
248
245
  )
249
246
 
250
- # Check for basic tag structure
251
- if not re.match(r'^<[^>]+>$', jsx_content):
252
- # More complex JSX, check for nested structure
253
- stack = []
254
- i = 0
255
- while i < len(jsx_content):
256
- if jsx_content[i] == '<':
257
- if i + 1 < len(jsx_content) and jsx_content[i + 1] == '/':
258
- # Closing tag
259
- if not stack:
260
- raise JSXSyntaxError(
261
- f"Unexpected closing tag",
262
- file_path=file_path
263
- )
264
- stack.pop()
265
- i += 1
266
- elif i + 1 < len(jsx_content) and jsx_content[i + 1] == '!':
267
- # Comment or DOCTYPE, skip to next '>'
268
- i = jsx_content.find('>', i)
269
- if i == -1:
270
- raise JSXSyntaxError(
271
- f"Unclosed comment/DOCTYPE",
272
- file_path=file_path
273
- )
274
- else:
275
- # Opening tag
276
- tag_end = jsx_content.find('>', i)
277
- if tag_end == -1:
278
- raise JSXSyntaxError(
279
- f"Unclosed opening tag",
280
- file_path=file_path
281
- )
282
-
283
- tag_content = jsx_content[i + 1:tag_end]
284
- if not tag_content.endswith('/'):
285
- # Not self-closing, add to stack
286
- tag_name = re.match(r'^\w+', tag_content)
287
- if tag_name:
288
- stack.append(tag_name.group(0))
289
- i = tag_end + 1
290
- else:
291
- i += 1
292
-
293
- if stack:
294
- raise JSXSyntaxError(
295
- f"Unclosed tag(s): {', '.join(stack)}",
296
- file_path=file_path
297
- )
247
+ # Instead of doing adhoc tag matching we can leverage the
248
+ # shared parser; this will catch real syntax errors and avoid
249
+ # false positives. The parser returns either a JSXElement or a
250
+ # string, but will raise exceptions for malformed input.
251
+ try:
252
+ from .true_jsx import parser
253
+ parser.parse_jsx(jsx_content)
254
+ except Exception as e:
255
+ # Rewrap parser errors as JSXSyntaxError for consistency
256
+ raise JSXSyntaxError(str(e), file_path=file_path)
298
257
 
258
+ def _check_balanced_tags(self, jsx_str: str, file_path: str = None):
259
+ """Ensure opening and closing tags are properly nested in a JSX block"""
260
+ stack = []
261
+ # simple regex to capture tags; self-closing tags end with '/>'
262
+ tag_pattern = re.compile(r'<(/?)([a-zA-Z][a-zA-Z0-9]*)[^>]*?(/?)>')
263
+ for match in tag_pattern.finditer(jsx_str):
264
+ closing = match.group(1) == '/'
265
+ tag = match.group(2)
266
+ self_close = match.group(3) == '/'
267
+ if closing:
268
+ if not stack or stack[-1] != tag:
269
+ raise JSXSyntaxError(
270
+ f"Malformed JSX: closing tag </{tag}> does not match open "+
271
+ (f"<{stack[-1]}>" if stack else "(none)"),
272
+ file_path=file_path
273
+ )
274
+ stack.pop()
275
+ elif not self_close:
276
+ stack.append(tag)
277
+ if stack:
278
+ raise JSXSyntaxError(
279
+ f"Unclosed JSX tag(s): {','.join(stack)}",
280
+ file_path=file_path
281
+ )
282
+
299
283
  def _get_line_number(self, content: str, position: int) -> int:
300
284
  """Get line number for a given position in content"""
301
285
  lines_before = content[:position].count('\n')
@@ -55,7 +55,14 @@ class NextPyApp:
55
55
  self.router = Router(str(self.pages_dir), str(self.templates_dir))
56
56
  else:
57
57
  self.router = ComponentRouter(str(self.pages_dir), str(self.templates_dir))
58
- self.renderer = ComponentRenderer(
58
+ # keep separate renderers for templates (Jinja) and components
59
+ from ..core import Renderer
60
+ self.template_renderer = Renderer(
61
+ templates_dir=str(self.templates_dir),
62
+ pages_dir=str(self.pages_dir),
63
+ public_dir=str(self.public_dir),
64
+ )
65
+ self.component_renderer = ComponentRenderer(
59
66
  debug=debug
60
67
  )
61
68
 
@@ -77,6 +84,14 @@ class NextPyApp:
77
84
  self._setup_middleware()
78
85
  self._setup_static_files()
79
86
  self._setup_routes()
87
+ # optionally compile tailwind CSS after routes are registered
88
+ try:
89
+ self._ensure_tailwind_compiled()
90
+ except Exception as e:
91
+ if self.debug:
92
+ print(f"Tailwind check/build failed: {e}")
93
+ # optionally compile tailwind at startup if requested
94
+ self._ensure_tailwind_compiled()
80
95
 
81
96
  def _setup_middleware(self) -> None:
82
97
  """Configure middleware"""
@@ -111,12 +126,36 @@ class NextPyApp:
111
126
  def _setup_static_files(self) -> None:
112
127
  """Mount static file directories"""
113
128
  if self.public_dir.exists():
129
+ # Mount public directory under /static to avoid intercepting all
130
+ # requests. Mounting at "/" previously caused every request to be
131
+ # handled by StaticFiles first, which returned 404 for dynamic
132
+ # pages and API routes. In a future release we might add a
133
+ # middleware that checks for a file in `public_dir` and serves it
134
+ # before falling back to the app, thus preserving Next.js semantics.
114
135
  self.app.mount(
115
136
  "/static",
116
137
  StaticFiles(directory=str(self.public_dir)),
117
- name="static",
138
+ name="public",
118
139
  )
119
-
140
+ # Ensure commonly referenced compiled Tailwind CSS paths are
141
+ # available. Templates may link to `/tailwind.css` or
142
+ # `/public/tailwind.css`; mounting under `/static` would
143
+ # expose the file at `/static/tailwind.css`. For compatibility
144
+ # add explicit routes that serve the compiled file if present.
145
+ try:
146
+ tailwind_file = self.public_dir / "tailwind.css"
147
+ if tailwind_file.exists():
148
+ from starlette.responses import FileResponse
149
+
150
+ def _tailwind(request, _path=str(tailwind_file)):
151
+ return FileResponse(_path)
152
+
153
+ # Register both commonly used locations
154
+ self.app.add_route("/tailwind.css", _tailwind, methods=["GET"])
155
+ self.app.add_route("/public/tailwind.css", _tailwind, methods=["GET"])
156
+ except Exception:
157
+ # Non-fatal: continue if registration fails
158
+ pass
120
159
  nextpy_static = self.out_dir / "_nextpy" / "static"
121
160
  if nextpy_static.exists():
122
161
  self.app.mount(
@@ -132,9 +171,11 @@ class NextPyApp:
132
171
  for route in self.router.get_all_routes():
133
172
  if route.is_api:
134
173
  # API routes - FIXED: use default argument to capture route correctly
174
+ from fastapi import Request
135
175
  def create_api_handler(route_obj):
136
- def api_handler(request, route=route_obj):
137
- return self._handle_api_request(request, route, {})
176
+ async def api_handler(request: Request, route=route_obj):
177
+ # delegate to async handler
178
+ return await self._handle_api_request(request, route, {})
138
179
  return api_handler
139
180
 
140
181
  self.app.add_api_route(
@@ -251,7 +292,7 @@ class NextPyApp:
251
292
 
252
293
  template_name = self._get_template_name(route, module)
253
294
 
254
- html = await self.renderer.render_async(
295
+ html = await self.template_renderer.render_async(
255
296
  template_name,
256
297
  context={
257
298
  **props,
@@ -334,6 +375,42 @@ class NextPyApp:
334
375
  status_code=500,
335
376
  )
336
377
 
378
+ def _ensure_tailwind_compiled(self) -> None:
379
+ """Conditionally build Tailwind CSS if env var is set and file is outdated"""
380
+ if os.getenv("NEXTPY_AUTO_BUILD_TAILWIND", "false").lower() != "true":
381
+ return
382
+ tailwind_file = self.public_dir / "tailwind.css"
383
+ styles = Path("styles.css")
384
+ config = Path("tailwind.config.js")
385
+ needs_build = False
386
+ if not tailwind_file.exists():
387
+ needs_build = True
388
+ else:
389
+ try:
390
+ mtime = tailwind_file.stat().st_mtime
391
+ if styles.exists() and styles.stat().st_mtime > mtime:
392
+ needs_build = True
393
+ if config.exists() and config.stat().st_mtime > mtime:
394
+ needs_build = True
395
+ except Exception:
396
+ needs_build = True
397
+ if needs_build:
398
+ self._run_tailwind_build()
399
+
400
+ def _run_tailwind_build(self) -> None:
401
+ """Invoke npm to compile Tailwind CSS"""
402
+ import subprocess, shutil
403
+ npm = shutil.which("npm")
404
+ if not npm:
405
+ print("[NextPy] npm not available; cannot build Tailwind CSS")
406
+ return
407
+ print("[NextPy] running tailwind CSS build...")
408
+ try:
409
+ subprocess.run([npm, "ci"], check=False)
410
+ subprocess.run([npm, "run", "build:tailwind"], check=False)
411
+ except Exception as e:
412
+ print(f"[NextPy] tailwind build failed: {e}")
413
+
337
414
  def _get_template_name(self, route, module: Optional[Any] = None) -> str:
338
415
  """Get the template name for a route"""
339
416
  if module and hasattr(module, "get_template"):
@@ -356,7 +433,7 @@ class NextPyApp:
356
433
  async def _render_404(self, request: Request) -> HTMLResponse:
357
434
  """Render the 404 page"""
358
435
  try:
359
- html = await self.renderer.render_async(
436
+ html = await self.template_renderer.render_async(
360
437
  "_404.html",
361
438
  context={"request": request},
362
439
  )
@@ -410,7 +487,7 @@ class NextPyApp:
410
487
  }
411
488
 
412
489
  try:
413
- html = await self.renderer.render_async(
490
+ html = await self.template_renderer.render_async(
414
491
  "_jsx_error.html",
415
492
  context={
416
493
  "request": request,
@@ -432,7 +509,7 @@ class NextPyApp:
432
509
  }
433
510
 
434
511
  try:
435
- html = await self.renderer.render_async(
512
+ html = await self.template_renderer.render_async(
436
513
  "_import_error.html",
437
514
  context={
438
515
  "request": request,
@@ -454,7 +531,7 @@ class NextPyApp:
454
531
  }
455
532
 
456
533
  try:
457
- html = await self.renderer.render_async(
534
+ html = await self.template_renderer.render_async(
458
535
  "_value_error.html",
459
536
  context={
460
537
  "request": request,
@@ -476,7 +553,7 @@ class NextPyApp:
476
553
  }
477
554
 
478
555
  try:
479
- html = await self.renderer.render_async(
556
+ html = await self.template_renderer.render_async(
480
557
  "_attribute_error.html",
481
558
  context={
482
559
  "request": request,
@@ -498,7 +575,7 @@ class NextPyApp:
498
575
  }
499
576
 
500
577
  try:
501
- html = await self.renderer.render_async(
578
+ html = await self.template_renderer.render_async(
502
579
  "_file_error.html",
503
580
  context={
504
581
  "request": request,
@@ -520,7 +597,7 @@ class NextPyApp:
520
597
  }
521
598
 
522
599
  try:
523
- html = await self.renderer.render_async(
600
+ html = await self.template_renderer.render_async(
524
601
  "_network_error.html",
525
602
  context={
526
603
  "request": request,
@@ -546,7 +623,7 @@ class NextPyApp:
546
623
  error_details["traceback"] = traceback.format_exc()
547
624
 
548
625
  try:
549
- html = await self.renderer.render_async(
626
+ html = await self.template_renderer.render_async(
550
627
  "_error.html",
551
628
  context={
552
629
  "request": request,
@@ -56,7 +56,8 @@ async def render_error_page(error: Dict[str, Any]) -> HTMLResponse:
56
56
  <html>
57
57
  <head>
58
58
  <title>NextPy Error</title>
59
- <script src="https://cdn.tailwindcss.com"></script>
59
+ <!-- rely on compiled CSS instead of CDN -->
60
+ <link href="/tailwind.css" rel="stylesheet">
60
61
  <style>
61
62
  .animate-pulse {{ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }}
62
63
  @keyframes pulse {{
@@ -66,23 +67,23 @@ async def render_error_page(error: Dict[str, Any]) -> HTMLResponse:
66
67
  </style>
67
68
  </head>
68
69
  <body class="bg-gray-900">
69
- <div class="fixed bottom-0 left-0 right-0 z-50 bg-red-950 text-red-100 border-t-4 border-red-600 shadow-2xl">
70
+ <div class="fixed bottom-0 left-0 right-0 z-50 text-red-100 border-t-4 border-red-600 shadow-2xl bg-red-950">
70
71
  <div class="max-w-full">
71
72
  <div class="flex items-center justify-between p-4 bg-red-900">
72
73
  <div class="flex items-center gap-3">
73
74
  <span class="text-2xl">⚠️</span>
74
75
  <div>
75
- <h3 class="font-bold text-lg">{error.get('type', 'Error')}</h3>
76
+ <h3 class="text-lg font-bold">{error.get('type', 'Error')}</h3>
76
77
  <p class="text-sm text-red-200">{error.get('message', 'An error occurred')}</p>
77
78
  </div>
78
79
  </div>
79
80
  </div>
80
81
  <div class="p-4">
81
- <h4 class="font-mono font-bold text-sm mb-2">Traceback:</h4>
82
- <pre class="font-mono text-xs bg-black bg-opacity-30 p-3 rounded overflow-x-auto text-red-100">{error.get('traceback', '')}</pre>
82
+ <h4 class="mb-2 font-mono text-sm font-bold">Traceback:</h4>
83
+ <pre class="p-3 overflow-x-auto font-mono text-xs text-red-100 bg-black rounded bg-opacity-30">{error.get('traceback', '')}</pre>
83
84
  </div>
84
85
  <div class="p-4 border-t border-red-800">
85
- <h4 class="font-mono font-bold text-sm mb-2">Location:</h4>
86
+ <h4 class="mb-2 font-mono text-sm font-bold">Location:</h4>
86
87
  <p class="text-sm"><span class="text-red-300">{error.get('file', 'unknown')}</span> line <span class="font-bold">{error.get('line', '?')}</span></p>
87
88
  </div>
88
89
  </div>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextpy-framework
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: A Python web framework inspired by Next.js with file-based routing, SSR, and SSG and more
5
5
  Author: NextPy Team
6
6
  License: MIT
@@ -113,6 +113,21 @@ Visit `http://localhost:8000` to see your app!
113
113
 
114
114
  ---
115
115
 
116
+ ## ⚠️ Important notes
117
+
118
+ * The framework now adds a set of security headers by default (CSP, X-Frame-Options, etc.) for safer deployments.
119
+ * You can request automatic Tailwind CSS compilation on startup by setting the
120
+ `NEXTPY_AUTO_BUILD_TAILWIND=true` environment variable. This requires `npm`
121
+ to be installed and will run `npm ci` followed by `npm run build:tailwind`.
122
+ * SQLAlchemy imports have been updated to avoid 2.0 deprecation warnings.
123
+ If you see such warnings upgrade your dependencies or pin the versions as
124
+ needed.
125
+
126
+
127
+ ---
128
+
129
+ ---
130
+
116
131
  ## 📝 Component Styles
117
132
 
118
133
  NextPy supports **9 different component styles** - choose what works best for you!
@@ -236,7 +251,7 @@ from nextpy.true_jsx import JSXComponent
236
251
  class Card(JSXComponent):
237
252
  def render(self):
238
253
  return (
239
- <div class="bg-white rounded-lg shadow-lg p-6">
254
+ <div class="p-6 bg-white rounded-lg shadow-lg">
240
255
  {self.props.get("children", "")}
241
256
  </div>
242
257
  )
@@ -332,7 +347,7 @@ class Layout(JSXComponent):
332
347
  return (
333
348
  <div class="min-h-screen bg-gray-100">
334
349
  <nav class="bg-white shadow">
335
- <div class="max-w-7xl mx-auto px-4">
350
+ <div class="px-4 mx-auto max-w-7xl">
336
351
  <div class="flex justify-between h-16">
337
352
  <div class="flex items-center">
338
353
  <h1 class="text-xl font-bold text-blue-600">NextPy</h1>
@@ -344,7 +359,7 @@ class Layout(JSXComponent):
344
359
  </div>
345
360
  </nav>
346
361
 
347
- <main class="max-w-7xl mx-auto py-6">
362
+ <main class="py-6 mx-auto max-w-7xl">
348
363
  {self.props.get("children", "")}
349
364
  </main>
350
365
  </div>
@@ -544,9 +559,9 @@ def Home(props=None):
544
559
  return (
545
560
  <div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
546
561
  <div class="text-center text-white">
547
- <h1 class="text-4xl font-bold mb-4">Hello NextPy!</h1>
548
- <p class="text-xl mb-8">Build modern web apps with Python</p>
549
- <button class="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">
562
+ <h1 class="mb-4 text-4xl font-bold">Hello NextPy!</h1>
563
+ <p class="mb-8 text-xl">Build modern web apps with Python</p>
564
+ <button class="px-6 py-3 font-semibold text-blue-600 transition-colors bg-white rounded-lg hover:bg-gray-100">
550
565
  Get Started
551
566
  </button>
552
567
  </div>
@@ -894,8 +909,8 @@ def HomePage(props=None):
894
909
  return (
895
910
  <div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
896
911
  <div class="text-center text-white">
897
- <h1 class="text-4xl font-bold mb-4">Hello NextPy!</h1>
898
- <button class="bg-white text-blue-600 px-6 py-3 rounded-lg hover:bg-gray-100 transition-colors">
912
+ <h1 class="mb-4 text-4xl font-bold">Hello NextPy!</h1>
913
+ <button class="px-6 py-3 text-blue-600 transition-colors bg-white rounded-lg hover:bg-gray-100">
899
914
  Get Started
900
915
  </button>
901
916
  </div>
@@ -72,4 +72,8 @@ pyproject.toml
72
72
  .nextpy_framework/nextpy_framework.egg-info/entry_points.txt
73
73
  .nextpy_framework/nextpy_framework.egg-info/requires.txt
74
74
  .nextpy_framework/nextpy_framework.egg-info/top_level.txt
75
- tests/test_routing.py
75
+ tests/test_jsx_edgecases.py
76
+ tests/test_jsx_preprocessor.py
77
+ tests/test_routing.py
78
+ tests/test_server_features.py
79
+ tests/test_tailwind_up_to_date.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextpy-framework
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: A Python web framework inspired by Next.js with file-based routing, SSR, and SSG and more
5
5
  Author: NextPy Team
6
6
  License: MIT
@@ -113,6 +113,21 @@ Visit `http://localhost:8000` to see your app!
113
113
 
114
114
  ---
115
115
 
116
+ ## ⚠️ Important notes
117
+
118
+ * The framework now adds a set of security headers by default (CSP, X-Frame-Options, etc.) for safer deployments.
119
+ * You can request automatic Tailwind CSS compilation on startup by setting the
120
+ `NEXTPY_AUTO_BUILD_TAILWIND=true` environment variable. This requires `npm`
121
+ to be installed and will run `npm ci` followed by `npm run build:tailwind`.
122
+ * SQLAlchemy imports have been updated to avoid 2.0 deprecation warnings.
123
+ If you see such warnings upgrade your dependencies or pin the versions as
124
+ needed.
125
+
126
+
127
+ ---
128
+
129
+ ---
130
+
116
131
  ## 📝 Component Styles
117
132
 
118
133
  NextPy supports **9 different component styles** - choose what works best for you!
@@ -236,7 +251,7 @@ from nextpy.true_jsx import JSXComponent
236
251
  class Card(JSXComponent):
237
252
  def render(self):
238
253
  return (
239
- <div class="bg-white rounded-lg shadow-lg p-6">
254
+ <div class="p-6 bg-white rounded-lg shadow-lg">
240
255
  {self.props.get("children", "")}
241
256
  </div>
242
257
  )
@@ -332,7 +347,7 @@ class Layout(JSXComponent):
332
347
  return (
333
348
  <div class="min-h-screen bg-gray-100">
334
349
  <nav class="bg-white shadow">
335
- <div class="max-w-7xl mx-auto px-4">
350
+ <div class="px-4 mx-auto max-w-7xl">
336
351
  <div class="flex justify-between h-16">
337
352
  <div class="flex items-center">
338
353
  <h1 class="text-xl font-bold text-blue-600">NextPy</h1>
@@ -344,7 +359,7 @@ class Layout(JSXComponent):
344
359
  </div>
345
360
  </nav>
346
361
 
347
- <main class="max-w-7xl mx-auto py-6">
362
+ <main class="py-6 mx-auto max-w-7xl">
348
363
  {self.props.get("children", "")}
349
364
  </main>
350
365
  </div>
@@ -544,9 +559,9 @@ def Home(props=None):
544
559
  return (
545
560
  <div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
546
561
  <div class="text-center text-white">
547
- <h1 class="text-4xl font-bold mb-4">Hello NextPy!</h1>
548
- <p class="text-xl mb-8">Build modern web apps with Python</p>
549
- <button class="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">
562
+ <h1 class="mb-4 text-4xl font-bold">Hello NextPy!</h1>
563
+ <p class="mb-8 text-xl">Build modern web apps with Python</p>
564
+ <button class="px-6 py-3 font-semibold text-blue-600 transition-colors bg-white rounded-lg hover:bg-gray-100">
550
565
  Get Started
551
566
  </button>
552
567
  </div>
@@ -894,8 +909,8 @@ def HomePage(props=None):
894
909
  return (
895
910
  <div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
896
911
  <div class="text-center text-white">
897
- <h1 class="text-4xl font-bold mb-4">Hello NextPy!</h1>
898
- <button class="bg-white text-blue-600 px-6 py-3 rounded-lg hover:bg-gray-100 transition-colors">
912
+ <h1 class="mb-4 text-4xl font-bold">Hello NextPy!</h1>
913
+ <button class="px-6 py-3 text-blue-600 transition-colors bg-white rounded-lg hover:bg-gray-100">
899
914
  Get Started
900
915
  </button>
901
916
  </div>
@@ -72,6 +72,21 @@ Visit `http://localhost:8000` to see your app!
72
72
 
73
73
  ---
74
74
 
75
+ ## ⚠️ Important notes
76
+
77
+ * The framework now adds a set of security headers by default (CSP, X-Frame-Options, etc.) for safer deployments.
78
+ * You can request automatic Tailwind CSS compilation on startup by setting the
79
+ `NEXTPY_AUTO_BUILD_TAILWIND=true` environment variable. This requires `npm`
80
+ to be installed and will run `npm ci` followed by `npm run build:tailwind`.
81
+ * SQLAlchemy imports have been updated to avoid 2.0 deprecation warnings.
82
+ If you see such warnings upgrade your dependencies or pin the versions as
83
+ needed.
84
+
85
+
86
+ ---
87
+
88
+ ---
89
+
75
90
  ## 📝 Component Styles
76
91
 
77
92
  NextPy supports **9 different component styles** - choose what works best for you!
@@ -195,7 +210,7 @@ from nextpy.true_jsx import JSXComponent
195
210
  class Card(JSXComponent):
196
211
  def render(self):
197
212
  return (
198
- <div class="bg-white rounded-lg shadow-lg p-6">
213
+ <div class="p-6 bg-white rounded-lg shadow-lg">
199
214
  {self.props.get("children", "")}
200
215
  </div>
201
216
  )
@@ -291,7 +306,7 @@ class Layout(JSXComponent):
291
306
  return (
292
307
  <div class="min-h-screen bg-gray-100">
293
308
  <nav class="bg-white shadow">
294
- <div class="max-w-7xl mx-auto px-4">
309
+ <div class="px-4 mx-auto max-w-7xl">
295
310
  <div class="flex justify-between h-16">
296
311
  <div class="flex items-center">
297
312
  <h1 class="text-xl font-bold text-blue-600">NextPy</h1>
@@ -303,7 +318,7 @@ class Layout(JSXComponent):
303
318
  </div>
304
319
  </nav>
305
320
 
306
- <main class="max-w-7xl mx-auto py-6">
321
+ <main class="py-6 mx-auto max-w-7xl">
307
322
  {self.props.get("children", "")}
308
323
  </main>
309
324
  </div>
@@ -503,9 +518,9 @@ def Home(props=None):
503
518
  return (
504
519
  <div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
505
520
  <div class="text-center text-white">
506
- <h1 class="text-4xl font-bold mb-4">Hello NextPy!</h1>
507
- <p class="text-xl mb-8">Build modern web apps with Python</p>
508
- <button class="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">
521
+ <h1 class="mb-4 text-4xl font-bold">Hello NextPy!</h1>
522
+ <p class="mb-8 text-xl">Build modern web apps with Python</p>
523
+ <button class="px-6 py-3 font-semibold text-blue-600 transition-colors bg-white rounded-lg hover:bg-gray-100">
509
524
  Get Started
510
525
  </button>
511
526
  </div>
@@ -853,8 +868,8 @@ def HomePage(props=None):
853
868
  return (
854
869
  <div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
855
870
  <div class="text-center text-white">
856
- <h1 class="text-4xl font-bold mb-4">Hello NextPy!</h1>
857
- <button class="bg-white text-blue-600 px-6 py-3 rounded-lg hover:bg-gray-100 transition-colors">
871
+ <h1 class="mb-4 text-4xl font-bold">Hello NextPy!</h1>
872
+ <button class="px-6 py-3 text-blue-600 transition-colors bg-white rounded-lg hover:bg-gray-100">
858
873
  Get Started
859
874
  </button>
860
875
  </div>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nextpy-framework"
7
- version = "2.4.6"
7
+ version = "2.4.8"
8
8
  description = "A Python web framework inspired by Next.js with file-based routing, SSR, and SSG and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,32 @@
1
+ import pytest
2
+ from nextpy.jsx_preprocessor import JSXPreprocessor, JSXSyntaxError
3
+
4
+
5
+ def test_nested_tags_transformed():
6
+ src = '''def Comp():
7
+ return (
8
+ <div class="outer"><span><strong>Hi</strong></span></div>
9
+ )
10
+ '''
11
+ out = JSXPreprocessor().preprocess_content(src)
12
+ assert 'jsx("<div class=\"outer\"><span><strong>Hi</strong></span></div>")' in out or 'jsx("<div' in out
13
+
14
+
15
+ def test_self_closing_tags():
16
+ src = '''def Comp():
17
+ return (
18
+ <div><img src="/img.png"/><br/></div>
19
+ )
20
+ '''
21
+ out = JSXPreprocessor().preprocess_content(src)
22
+ assert 'img' in out and 'br' in out
23
+
24
+
25
+ def test_unclosed_tag_raises():
26
+ src = '''def Comp():
27
+ return (
28
+ <div><span>Broken</div>
29
+ )
30
+ '''
31
+ with pytest.raises(JSXSyntaxError):
32
+ JSXPreprocessor().preprocess_content(src)
@@ -0,0 +1,66 @@
1
+ import pytest
2
+
3
+ from nextpy.jsx_preprocessor import JSXPreprocessor, JSXSyntaxError
4
+
5
+ pre = JSXPreprocessor()
6
+
7
+
8
+ def normalize(s: str) -> str:
9
+ # strip leading/trailing whitespace and collapse newlines for easier comparison
10
+ return "\n".join(line.rstrip() for line in s.strip().splitlines())
11
+
12
+
13
+ def test_simple_div_transformation():
14
+ src = """def Comp():
15
+ return (
16
+ <div class=\"foo\">Hello</div>
17
+ )
18
+ """
19
+ out = pre.preprocess_content(src)
20
+ # output may escape quotes with backslashes, normalize for check
21
+ assert 'jsx("<div class=\"foo\">Hello</div>")' in out or 'jsx("<div class=\\"foo\\">Hello</div>")' in out
22
+ assert 'from nextpy.true_jsx import jsx' in out
23
+
24
+
25
+ def test_nested_elements():
26
+ src = """def Comp():
27
+ return (<div><span>Hi</span></div>)
28
+ """
29
+ out = pre.preprocess_content(src)
30
+ assert 'jsx("<div><span>Hi</span></div>")' in out
31
+
32
+
33
+ def test_self_closing_tag():
34
+ src = """def Comp():
35
+ return (<img src=\"/img.png\" />)
36
+ """
37
+ out = pre.preprocess_content(src)
38
+ assert 'jsx("<img src=\"/img.png\" />")' in out or 'jsx("<img src=\\"/img.png\\" />")' in out
39
+
40
+
41
+ def test_attributes_with_braces():
42
+ # braces should be preserved inside JSX string
43
+ src = """def Comp():
44
+ return (<div>{value}</div>)
45
+ """
46
+ out = pre.preprocess_content(src)
47
+ assert '{value}' in out
48
+
49
+
50
+ def test_unclosed_tag_raises_error():
51
+ src = """def Comp():
52
+ return (<div><span></div>)
53
+ """
54
+ with pytest.raises(JSXSyntaxError) as excinfo:
55
+ pre.preprocess_content(src)
56
+ msg = str(excinfo.value)
57
+ assert "Unclosed" in msg or "Malformed JSX" in msg
58
+
59
+
60
+ def test_invalid_block_does_nothing():
61
+ # if no JSX tags are present the preprocessor should leave content untouched
62
+ src = """def Comp():
63
+ return (div>oops</div>)
64
+ """
65
+ out = pre.preprocess_content(src)
66
+ assert "div>oops" in out
@@ -0,0 +1,47 @@
1
+ import os
2
+ import subprocess
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from fastapi.testclient import TestClient
7
+
8
+ from nextpy.server.app import create_app
9
+
10
+
11
+ def test_security_headers_set():
12
+ app = create_app(debug=True)
13
+ client = TestClient(app)
14
+ resp = client.get("/")
15
+ # check handful of headers
16
+ assert resp.headers.get("X-Content-Type-Options") == "nosniff"
17
+ assert resp.headers.get("X-Frame-Options") == "DENY"
18
+ assert "Content-Security-Policy" in resp.headers
19
+ assert resp.headers.get("Referrer-Policy") == "strict-origin-when-cross-origin"
20
+
21
+
22
+ def test_auto_tailwind_build(monkeypatch, tmp_path, capsys):
23
+ # simulate absence of tailwind.css
24
+ os.environ["NEXTPY_AUTO_BUILD_TAILWIND"] = "true"
25
+ # create dummy public directory
26
+ public = tmp_path / "public"
27
+ public.mkdir()
28
+ # change cwd to tmp
29
+ cwd = Path.cwd()
30
+ try:
31
+ os.chdir(tmp_path)
32
+ # monkeypatch npm presence and subprocess.run
33
+ monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/npm" if name == "npm" else None)
34
+ calls = []
35
+ def fake_run(cmd, check=False):
36
+ calls.append(cmd)
37
+ class R:
38
+ returncode = 0
39
+ return R()
40
+ monkeypatch.setattr(subprocess, "run", fake_run)
41
+ # create app which should trigger build
42
+ app = create_app(debug=True)
43
+ # ensure our fake_run was invoked
44
+ assert any("build:tailwind" in str(cmd) for cmd in calls)
45
+ finally:
46
+ os.chdir(cwd)
47
+ os.environ.pop("NEXTPY_AUTO_BUILD_TAILWIND", None)
@@ -0,0 +1,22 @@
1
+ from pathlib import Path
2
+ import pytest
3
+
4
+
5
+ def test_tailwind_compiled_up_to_date():
6
+ root = Path.cwd()
7
+ public_css = root / 'public' / 'tailwind.css'
8
+ styles = root / 'styles.css'
9
+ config = root / 'tailwind.config.js'
10
+
11
+ assert public_css.exists(), "public/tailwind.css is missing — run `npm run build:tailwind`"
12
+
13
+ public_mtime = public_css.stat().st_mtime
14
+ styles_mtime = styles.stat().st_mtime if styles.exists() else 0
15
+ config_mtime = config.stat().st_mtime if config.exists() else 0
16
+
17
+ assert public_mtime >= styles_mtime, (
18
+ "public/tailwind.css is older than styles.css — recompile with `npm run build:tailwind`"
19
+ )
20
+ assert public_mtime >= config_mtime, (
21
+ "public/tailwind.css is older than tailwind.config.js — recompile with `npm run build:tailwind`"
22
+ )