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.
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/cli.py +9 -10
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/db.py +1 -1
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx_preprocessor.py +35 -51
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/app.py +91 -14
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/debug.py +7 -6
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8/.nextpy_framework/nextpy_framework.egg-info}/PKG-INFO +24 -9
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/SOURCES.txt +5 -1
- {nextpy_framework-2.4.6/.nextpy_framework/nextpy_framework.egg-info → nextpy_framework-2.4.8}/PKG-INFO +24 -9
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/README.md +23 -8
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/pyproject.toml +1 -1
- nextpy_framework-2.4.8/tests/test_jsx_edgecases.py +32 -0
- nextpy_framework-2.4.8/tests/test_jsx_preprocessor.py +66 -0
- nextpy_framework-2.4.8/tests/test_server_features.py +47 -0
- nextpy_framework-2.4.8/tests/test_tailwind_up_to_date.py +22 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/__init__.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/auth.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/builder.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/__init__.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/debug/AutoDebug.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/debug/DebugIcon.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/debug/DebugIconFixed.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/feedback.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/form.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/head.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/hooks_provider.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/image.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/layout.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/link.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/loader.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/navigation.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/toast.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/ui.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/visual.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/config.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/__init__.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/builder.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/component_renderer.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/component_router.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/data_fetching.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/demo_pages_simple.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/demo_router.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/renderer.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/router.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/sync.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/dev_server.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/dev_tools.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/errors.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks_provider.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks_provider_new.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx_transformer.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/main.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/performance.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/__init__.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/base.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/builtin.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/config.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/py.typed +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/security.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/__init__.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/middleware.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/true_jsx.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/__init__.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/cache.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/email.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/file_upload.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/logging.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/search.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/seo.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/validators.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/websocket.py +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/dependency_links.txt +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/entry_points.txt +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/requires.txt +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy_framework.egg-info/top_level.txt +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/LICENSE +0 -0
- {nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/setup.cfg +0 -0
- {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.
|
|
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>
|
|
@@ -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.
|
|
10
|
+
from sqlalchemy.orm import declarative_base
|
|
11
11
|
|
|
12
12
|
Base = declarative_base()
|
|
13
13
|
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.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,42 @@ class JSXPreprocessor:
|
|
|
247
244
|
file_path=file_path
|
|
248
245
|
)
|
|
249
246
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextpy-framework
|
|
3
|
-
Version: 2.4.
|
|
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
|
|
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.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
|
|
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.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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/feedback.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/form.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/head.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/image.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/layout.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/link.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/loader.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/navigation.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/toast.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/components/visual.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/component_router.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/data_fetching.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/core/demo_pages_simple.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.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.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks_provider.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/hooks_provider_new.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/jsx_transformer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/builtin.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/plugins/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/__init__.py
RENAMED
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/server/middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.nextpy_framework/nextpy/utils/file_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextpy_framework-2.4.6 → nextpy_framework-2.4.8}/.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
|