nextpy-framework 2.4.7__tar.gz → 2.4.9__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.
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/cli.py +9 -8
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/component_renderer.py +4 -2
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/db.py +1 -1
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/jsx_preprocessor.py +36 -51
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/server/app.py +92 -18
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/server/debug.py +7 -6
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/true_jsx.py +58 -16
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9/.nextpy_framework/nextpy_framework.egg-info}/PKG-INFO +24 -9
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy_framework.egg-info/SOURCES.txt +5 -1
- {nextpy_framework-2.4.7/.nextpy_framework/nextpy_framework.egg-info → nextpy_framework-2.4.9}/PKG-INFO +24 -9
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/README.md +23 -8
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/pyproject.toml +1 -1
- nextpy_framework-2.4.9/tests/test_jsx_edgecases.py +32 -0
- nextpy_framework-2.4.9/tests/test_jsx_preprocessor.py +66 -0
- nextpy_framework-2.4.9/tests/test_server_features.py +47 -0
- nextpy_framework-2.4.9/tests/test_tailwind_up_to_date.py +22 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/__init__.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/auth.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/builder.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/__init__.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/debug/AutoDebug.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/debug/DebugIcon.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/debug/DebugIconFixed.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/feedback.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/form.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/head.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/hooks_provider.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/image.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/layout.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/link.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/loader.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/navigation.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/toast.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/ui.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/visual.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/config.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/__init__.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/builder.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/component_router.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/data_fetching.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/demo_pages_simple.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/demo_router.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/renderer.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/router.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/sync.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/dev_server.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/dev_tools.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/errors.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/hooks.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/hooks_provider.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/hooks_provider_new.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/jsx.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/jsx_transformer.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/main.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/performance.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/__init__.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/base.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/builtin.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/config.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/py.typed +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/security.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/server/__init__.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/server/middleware.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/__init__.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/cache.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/email.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/file_upload.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/logging.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/search.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/seo.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/validators.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/websocket.py +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy_framework.egg-info/dependency_links.txt +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy_framework.egg-info/entry_points.txt +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy_framework.egg-info/requires.txt +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy_framework.egg-info/top_level.txt +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/LICENSE +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/setup.cfg +0 -0
- {nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/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.
|
|
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
|
-
|
|
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
|
-
<
|
|
905
|
+
<link href="/tailwind.css" rel="stylesheet">
|
|
905
906
|
</head>
|
|
906
|
-
<body class="min-h-screen bg-gray-100
|
|
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
|
|
909
|
-
<p class="text-xl text-gray-600
|
|
910
|
-
<a href="/" class="px-6 py-3 bg-blue-600
|
|
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>
|
|
@@ -2266,7 +2267,7 @@ watchdog>=3.0.0
|
|
|
2266
2267
|
python-multipart>=0.0.6
|
|
2267
2268
|
pillow>=10.0.0
|
|
2268
2269
|
aiofiles>=23.0.0
|
|
2269
|
-
httpx>=0.24.0
|
|
2270
|
+
httpx>=0.24.0,<0.25 # compatibility with Starlette TestClient
|
|
2270
2271
|
sqlalchemy>=2.0.0
|
|
2271
2272
|
python-dotenv>=1.0.0
|
|
2272
2273
|
pyjwt>=2.8.0
|
|
@@ -219,8 +219,8 @@ class ComponentRenderer:
|
|
|
219
219
|
# It's already a JSX element
|
|
220
220
|
rendered = component
|
|
221
221
|
|
|
222
|
-
# Convert to HTML
|
|
223
|
-
html = render_jsx(rendered)
|
|
222
|
+
# Convert to HTML, passing page props as context for {expressions}
|
|
223
|
+
html = render_jsx(rendered, page_props)
|
|
224
224
|
|
|
225
225
|
# Inject debug icon in development mode
|
|
226
226
|
if AUTO_DEBUG_AVAILABLE and should_show_debug():
|
|
@@ -249,6 +249,7 @@ class ComponentRenderer:
|
|
|
249
249
|
title = props.get('title', 'NextPy App')
|
|
250
250
|
description = props.get('description', 'NextPy Application')
|
|
251
251
|
|
|
252
|
+
# Include local Tailwind stylesheet by default for created apps
|
|
252
253
|
return f"""<!DOCTYPE html>
|
|
253
254
|
<html lang="en">
|
|
254
255
|
<head>
|
|
@@ -256,6 +257,7 @@ class ComponentRenderer:
|
|
|
256
257
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
257
258
|
<title>{title}</title>
|
|
258
259
|
<meta name="description" content="{description}">
|
|
260
|
+
<link rel="stylesheet" href="/tailwind.css">
|
|
259
261
|
<style>
|
|
260
262
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
261
263
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }}
|
|
@@ -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.
|
|
10
|
+
from sqlalchemy.orm import declarative_base
|
|
11
11
|
|
|
12
12
|
Base = declarative_base()
|
|
13
13
|
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/jsx_preprocessor.py
RENAMED
|
@@ -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,43 @@ class JSXPreprocessor:
|
|
|
247
244
|
file_path=file_path
|
|
248
245
|
)
|
|
249
246
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
# First perform balanced-tag checks to catch unclosed/mismatched tags
|
|
248
|
+
self._check_balanced_tags(jsx_content, file_path=file_path)
|
|
249
|
+
self._validate_jsx_structure(jsx_content, file_path=file_path)
|
|
250
|
+
|
|
251
|
+
# Then delegate to the shared parser for deeper validation
|
|
252
|
+
try:
|
|
253
|
+
from .true_jsx import parser
|
|
254
|
+
parser.parse_jsx(jsx_content)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
# Rewrap parser errors as JSXSyntaxError for consistency
|
|
257
|
+
raise JSXSyntaxError(str(e), file_path=file_path)
|
|
298
258
|
|
|
259
|
+
def _check_balanced_tags(self, jsx_str: str, file_path: str = None):
|
|
260
|
+
"""Ensure opening and closing tags are properly nested in a JSX block"""
|
|
261
|
+
stack = []
|
|
262
|
+
# simple regex to capture tags; self-closing tags end with '/>'
|
|
263
|
+
tag_pattern = re.compile(r'<(/?)([a-zA-Z][a-zA-Z0-9]*)[^>]*?(/?)>')
|
|
264
|
+
for match in tag_pattern.finditer(jsx_str):
|
|
265
|
+
closing = match.group(1) == '/'
|
|
266
|
+
tag = match.group(2)
|
|
267
|
+
self_close = match.group(3) == '/'
|
|
268
|
+
if closing:
|
|
269
|
+
if not stack or stack[-1] != tag:
|
|
270
|
+
raise JSXSyntaxError(
|
|
271
|
+
f"Malformed JSX: closing tag </{tag}> does not match open "+
|
|
272
|
+
(f"<{stack[-1]}>" if stack else "(none)"),
|
|
273
|
+
file_path=file_path
|
|
274
|
+
)
|
|
275
|
+
stack.pop()
|
|
276
|
+
elif not self_close:
|
|
277
|
+
stack.append(tag)
|
|
278
|
+
if stack:
|
|
279
|
+
raise JSXSyntaxError(
|
|
280
|
+
f"Unclosed JSX tag(s): {','.join(stack)}",
|
|
281
|
+
file_path=file_path
|
|
282
|
+
)
|
|
283
|
+
|
|
299
284
|
def _get_line_number(self, content: str, position: int) -> int:
|
|
300
285
|
"""Get line number for a given position in content"""
|
|
301
286
|
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
|
-
|
|
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,15 +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():
|
|
114
|
-
# Mount public directory
|
|
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.
|
|
115
135
|
self.app.mount(
|
|
116
|
-
"/",
|
|
117
|
-
"/static/",
|
|
118
|
-
"/public/",
|
|
136
|
+
"/static",
|
|
119
137
|
StaticFiles(directory=str(self.public_dir)),
|
|
120
|
-
name="public"
|
|
138
|
+
name="public",
|
|
121
139
|
)
|
|
122
|
-
|
|
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
|
|
123
159
|
nextpy_static = self.out_dir / "_nextpy" / "static"
|
|
124
160
|
if nextpy_static.exists():
|
|
125
161
|
self.app.mount(
|
|
@@ -135,9 +171,11 @@ class NextPyApp:
|
|
|
135
171
|
for route in self.router.get_all_routes():
|
|
136
172
|
if route.is_api:
|
|
137
173
|
# API routes - FIXED: use default argument to capture route correctly
|
|
174
|
+
from fastapi import Request
|
|
138
175
|
def create_api_handler(route_obj):
|
|
139
|
-
def api_handler(request, route=route_obj):
|
|
140
|
-
|
|
176
|
+
async def api_handler(request: Request, route=route_obj):
|
|
177
|
+
# delegate to async handler
|
|
178
|
+
return await self._handle_api_request(request, route, {})
|
|
141
179
|
return api_handler
|
|
142
180
|
|
|
143
181
|
self.app.add_api_route(
|
|
@@ -254,7 +292,7 @@ class NextPyApp:
|
|
|
254
292
|
|
|
255
293
|
template_name = self._get_template_name(route, module)
|
|
256
294
|
|
|
257
|
-
html = await self.
|
|
295
|
+
html = await self.template_renderer.render_async(
|
|
258
296
|
template_name,
|
|
259
297
|
context={
|
|
260
298
|
**props,
|
|
@@ -337,6 +375,42 @@ class NextPyApp:
|
|
|
337
375
|
status_code=500,
|
|
338
376
|
)
|
|
339
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
|
+
|
|
340
414
|
def _get_template_name(self, route, module: Optional[Any] = None) -> str:
|
|
341
415
|
"""Get the template name for a route"""
|
|
342
416
|
if module and hasattr(module, "get_template"):
|
|
@@ -359,7 +433,7 @@ class NextPyApp:
|
|
|
359
433
|
async def _render_404(self, request: Request) -> HTMLResponse:
|
|
360
434
|
"""Render the 404 page"""
|
|
361
435
|
try:
|
|
362
|
-
html = await self.
|
|
436
|
+
html = await self.template_renderer.render_async(
|
|
363
437
|
"_404.html",
|
|
364
438
|
context={"request": request},
|
|
365
439
|
)
|
|
@@ -413,7 +487,7 @@ class NextPyApp:
|
|
|
413
487
|
}
|
|
414
488
|
|
|
415
489
|
try:
|
|
416
|
-
html = await self.
|
|
490
|
+
html = await self.template_renderer.render_async(
|
|
417
491
|
"_jsx_error.html",
|
|
418
492
|
context={
|
|
419
493
|
"request": request,
|
|
@@ -435,7 +509,7 @@ class NextPyApp:
|
|
|
435
509
|
}
|
|
436
510
|
|
|
437
511
|
try:
|
|
438
|
-
html = await self.
|
|
512
|
+
html = await self.template_renderer.render_async(
|
|
439
513
|
"_import_error.html",
|
|
440
514
|
context={
|
|
441
515
|
"request": request,
|
|
@@ -457,7 +531,7 @@ class NextPyApp:
|
|
|
457
531
|
}
|
|
458
532
|
|
|
459
533
|
try:
|
|
460
|
-
html = await self.
|
|
534
|
+
html = await self.template_renderer.render_async(
|
|
461
535
|
"_value_error.html",
|
|
462
536
|
context={
|
|
463
537
|
"request": request,
|
|
@@ -479,7 +553,7 @@ class NextPyApp:
|
|
|
479
553
|
}
|
|
480
554
|
|
|
481
555
|
try:
|
|
482
|
-
html = await self.
|
|
556
|
+
html = await self.template_renderer.render_async(
|
|
483
557
|
"_attribute_error.html",
|
|
484
558
|
context={
|
|
485
559
|
"request": request,
|
|
@@ -501,7 +575,7 @@ class NextPyApp:
|
|
|
501
575
|
}
|
|
502
576
|
|
|
503
577
|
try:
|
|
504
|
-
html = await self.
|
|
578
|
+
html = await self.template_renderer.render_async(
|
|
505
579
|
"_file_error.html",
|
|
506
580
|
context={
|
|
507
581
|
"request": request,
|
|
@@ -523,7 +597,7 @@ class NextPyApp:
|
|
|
523
597
|
}
|
|
524
598
|
|
|
525
599
|
try:
|
|
526
|
-
html = await self.
|
|
600
|
+
html = await self.template_renderer.render_async(
|
|
527
601
|
"_network_error.html",
|
|
528
602
|
context={
|
|
529
603
|
"request": request,
|
|
@@ -549,7 +623,7 @@ class NextPyApp:
|
|
|
549
623
|
error_details["traceback"] = traceback.format_exc()
|
|
550
624
|
|
|
551
625
|
try:
|
|
552
|
-
html = await self.
|
|
626
|
+
html = await self.template_renderer.render_async(
|
|
553
627
|
"_error.html",
|
|
554
628
|
context={
|
|
555
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
|
-
|
|
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
|
|
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
|
|
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="
|
|
82
|
-
<pre class="font-mono text-xs bg-black bg-opacity-30
|
|
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="
|
|
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>
|
|
@@ -21,19 +21,43 @@ class JSXElement:
|
|
|
21
21
|
"""Convert to HTML string"""
|
|
22
22
|
return self.to_html()
|
|
23
23
|
|
|
24
|
-
def to_html(self) -> str:
|
|
25
|
-
"""Convert JSX element to HTML string
|
|
24
|
+
def to_html(self, context: Dict[str, Any] = None) -> str:
|
|
25
|
+
"""Convert JSX element to HTML string.
|
|
26
|
+
|
|
27
|
+
Evaluates any {expressions} in text nodes and prop values using `context`.
|
|
28
|
+
"""
|
|
26
29
|
# Build props string
|
|
27
30
|
props_str = ""
|
|
28
31
|
if self.props:
|
|
29
32
|
props_list = []
|
|
30
33
|
for key, value in self.props.items():
|
|
34
|
+
# Map React-style `className` to HTML `class`
|
|
35
|
+
attr_name = 'class' if key == 'className' else key
|
|
36
|
+
|
|
37
|
+
# Boolean attribute
|
|
31
38
|
if isinstance(value, bool) and value:
|
|
32
|
-
props_list.append(
|
|
39
|
+
props_list.append(attr_name)
|
|
33
40
|
elif value is not None and value != "":
|
|
41
|
+
# Skip event handlers
|
|
34
42
|
if key.startswith("on_") and callable(value):
|
|
35
43
|
continue
|
|
36
|
-
|
|
44
|
+
|
|
45
|
+
# If value looks like a stored expression token '{expr}', evaluate at render time
|
|
46
|
+
if isinstance(value, str) and value.startswith('{') and value.endswith('}') and context is not None:
|
|
47
|
+
inner = value[1:-1].strip()
|
|
48
|
+
try:
|
|
49
|
+
evaluated = str(eval(inner, {}, context))
|
|
50
|
+
except Exception:
|
|
51
|
+
evaluated = ''
|
|
52
|
+
props_list.append(f'{attr_name}="{evaluated}"')
|
|
53
|
+
# If string contains inline {expr} parts, evaluate those
|
|
54
|
+
elif isinstance(value, str) and '{' in value and '}' in value and context is not None:
|
|
55
|
+
from re import sub
|
|
56
|
+
evaluated = _evaluate_expressions_in_string(value, context)
|
|
57
|
+
props_list.append(f'{attr_name}="{evaluated}"')
|
|
58
|
+
else:
|
|
59
|
+
props_list.append(f'{attr_name}="{value}"')
|
|
60
|
+
|
|
37
61
|
props_str = " " + " ".join(props_list) if props_list else ""
|
|
38
62
|
|
|
39
63
|
# Build children string
|
|
@@ -42,9 +66,13 @@ class JSXElement:
|
|
|
42
66
|
children_parts = []
|
|
43
67
|
for child in self.children:
|
|
44
68
|
if isinstance(child, JSXElement):
|
|
45
|
-
children_parts.append(child.to_html())
|
|
69
|
+
children_parts.append(child.to_html(context))
|
|
46
70
|
else:
|
|
47
|
-
|
|
71
|
+
# Evaluate any {expressions} inside text nodes
|
|
72
|
+
if isinstance(child, str) and '{' in child and '}' in child and context is not None:
|
|
73
|
+
children_parts.append(_evaluate_expressions_in_string(child, context))
|
|
74
|
+
else:
|
|
75
|
+
children_parts.append(str(child))
|
|
48
76
|
children_str = "".join(children_parts)
|
|
49
77
|
|
|
50
78
|
# Handle self-closing tags
|
|
@@ -52,10 +80,10 @@ class JSXElement:
|
|
|
52
80
|
'img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base', 'col',
|
|
53
81
|
'embed', 'source', 'track', 'wbr', 'command', 'keygen', 'menuitem', 'param'
|
|
54
82
|
}
|
|
55
|
-
|
|
83
|
+
|
|
56
84
|
if self.tag in self_closing_tags and not children_str:
|
|
57
85
|
return f"<{self.tag}{props_str} />"
|
|
58
|
-
|
|
86
|
+
|
|
59
87
|
return f"<{self.tag}{props_str}>{children_str}</{self.tag}>"
|
|
60
88
|
|
|
61
89
|
|
|
@@ -79,11 +107,8 @@ class JSXParser:
|
|
|
79
107
|
if groups[0] and groups[1]: # {prop} syntax
|
|
80
108
|
prop_name = groups[0]
|
|
81
109
|
prop_value = groups[1].strip()
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
props[prop_name] = eval(prop_value)
|
|
85
|
-
except:
|
|
86
|
-
props[prop_name] = prop_value
|
|
110
|
+
# Store as a raw expression token to be evaluated at render time
|
|
111
|
+
props[prop_name] = '{' + prop_value + '}'
|
|
87
112
|
elif groups[2] and groups[3]: # "prop" syntax
|
|
88
113
|
props[groups[2]] = groups[3]
|
|
89
114
|
elif groups[4] and groups[5]: # 'prop' syntax
|
|
@@ -180,11 +205,28 @@ def jsx(jsx_str: str) -> JSXElement:
|
|
|
180
205
|
return parser.parse_jsx(jsx_str)
|
|
181
206
|
|
|
182
207
|
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
208
|
+
def _evaluate_expressions_in_string(s: str, context: Dict[str, Any]) -> str:
|
|
209
|
+
"""Find {expressions} in string `s` and evaluate them using `context`."""
|
|
210
|
+
def repl(match):
|
|
211
|
+
expr = match.group(1).strip()
|
|
212
|
+
try:
|
|
213
|
+
return str(eval(expr, {}, context or {}))
|
|
214
|
+
except Exception:
|
|
215
|
+
return ''
|
|
216
|
+
|
|
217
|
+
return re.sub(r'\{([^}]+)\}', repl, s)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def render_jsx(element, context: Dict[str, Any] = None) -> str:
|
|
221
|
+
"""Render JSX element to HTML string, evaluating {expressions} with `context`.
|
|
222
|
+
|
|
223
|
+
Usage: `render_jsx(element, context)`
|
|
224
|
+
"""
|
|
185
225
|
if isinstance(element, JSXElement):
|
|
186
|
-
return element.to_html()
|
|
226
|
+
return element.to_html(context)
|
|
187
227
|
elif isinstance(element, str):
|
|
228
|
+
if context and '{' in element and '}' in element:
|
|
229
|
+
return _evaluate_expressions_in_string(element, context)
|
|
188
230
|
return element
|
|
189
231
|
else:
|
|
190
232
|
return str(element)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextpy-framework
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.9
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
548
|
-
<p class="text-xl
|
|
549
|
-
<button class="
|
|
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
|
|
898
|
-
<button class="
|
|
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/
|
|
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.
|
|
3
|
+
Version: 2.4.9
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
548
|
-
<p class="text-xl
|
|
549
|
-
<button class="
|
|
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
|
|
898
|
-
<button class="
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
507
|
-
<p class="text-xl
|
|
508
|
-
<button class="
|
|
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
|
|
857
|
-
<button class="
|
|
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.
|
|
7
|
+
version = "2.4.9"
|
|
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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/feedback.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/form.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/head.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/image.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/layout.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/link.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/loader.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/navigation.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/toast.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/components/visual.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/component_router.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/data_fetching.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/demo_pages_simple.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/core/demo_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/hooks_provider.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/hooks_provider_new.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/jsx_transformer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/builtin.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/plugins/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/server/__init__.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/server/middleware.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/file_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.7 → nextpy_framework-2.4.9}/.nextpy_framework/nextpy/utils/validators.py
RENAMED
|
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
|