pyxle-framework 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/PKG-INFO +1 -1
  2. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/README.md +1 -1
  3. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/faq.md +1 -1
  4. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyproject.toml +1 -1
  5. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/manifest.py +6 -3
  6. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/__init__.py +2 -1
  7. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/index.js +1 -0
  8. pyxle_framework-0.2.2/pyxle/client/usePathname.jsx +35 -0
  9. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/client_files.py +67 -0
  10. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/registry.py +59 -0
  11. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/view.py +57 -7
  12. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_client_files.py +47 -0
  13. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_registry.py +65 -0
  14. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/.github/pyxle-logo.svg +0 -0
  15. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/.github/workflows/publish.yml +0 -0
  16. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/.gitignore +0 -0
  17. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/CLAUDE.md +0 -0
  18. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/LICENSE +0 -0
  19. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/Makefile +0 -0
  20. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/README.md +0 -0
  21. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/advanced/compiler-internals.md +0 -0
  22. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/advanced/ssr-pipeline.md +0 -0
  23. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/README.md +0 -0
  24. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/build-and-serve.md +0 -0
  25. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/cli.md +0 -0
  26. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/compiler.md +0 -0
  27. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/dev-server.md +0 -0
  28. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/overview.md +0 -0
  29. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/parser.md +0 -0
  30. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/pyxl-files.md +0 -0
  31. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/routing.md +0 -0
  32. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/runtime.md +0 -0
  33. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/ssr.md +0 -0
  34. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/data-loading.md +0 -0
  35. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/layouts.md +0 -0
  36. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/pyxl-files.md +0 -0
  37. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/routing.md +0 -0
  38. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/server-actions.md +0 -0
  39. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/getting-started/installation.md +0 -0
  40. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/getting-started/project-structure.md +0 -0
  41. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/getting-started/quick-start.md +0 -0
  42. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/api-routes.md +0 -0
  43. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/client-components.md +0 -0
  44. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/deployment.md +0 -0
  45. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/editor-setup.md +0 -0
  46. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/environment-variables.md +0 -0
  47. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/error-handling.md +0 -0
  48. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/for-ai-agents.md +0 -0
  49. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/head-management.md +0 -0
  50. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/middleware.md +0 -0
  51. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/migration-pyx-to-pyxl.md +0 -0
  52. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/security.md +0 -0
  53. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/styling.md +0 -0
  54. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/cli.md +0 -0
  55. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/client-api.md +0 -0
  56. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/configuration.md +0 -0
  57. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/runtime-api.md +0 -0
  58. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/__init__.py +0 -0
  59. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/__init__.py +0 -0
  60. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/pipeline.py +0 -0
  61. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/vite.py +0 -0
  62. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/assets.py +0 -0
  63. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/init.py +0 -0
  64. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/logger.py +0 -0
  65. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/scaffold.py +0 -0
  66. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/templates.py +0 -0
  67. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/ClientOnly.jsx +0 -0
  68. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Form.jsx +0 -0
  69. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Head.jsx +0 -0
  70. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Image.jsx +0 -0
  71. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Script.jsx +0 -0
  72. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/useAction.jsx +0 -0
  73. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/__init__.py +0 -0
  74. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/core.py +0 -0
  75. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/exceptions.py +0 -0
  76. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/jsx_imports.py +0 -0
  77. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/jsx_parser.py +0 -0
  78. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/model.py +0 -0
  79. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/parser.py +0 -0
  80. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/writers.py +0 -0
  81. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/config.py +0 -0
  82. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/__init__.py +0 -0
  83. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/_security.py +0 -0
  84. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/build.py +0 -0
  85. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/builder.py +0 -0
  86. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/csrf.py +0 -0
  87. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/error_pages.py +0 -0
  88. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/layouts.py +0 -0
  89. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/middleware.py +0 -0
  90. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/overlay.py +0 -0
  91. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/path_utils.py +0 -0
  92. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/proxy.py +0 -0
  93. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/route_hooks.py +0 -0
  94. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/routes.py +0 -0
  95. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/scanner.py +0 -0
  96. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/scripts.py +0 -0
  97. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/settings.py +0 -0
  98. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/starlette_app.py +0 -0
  99. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/styles.py +0 -0
  100. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/tailwind.py +0 -0
  101. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/vite.py +0 -0
  102. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/watcher.py +0 -0
  103. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/env.py +0 -0
  104. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/routing/__init__.py +0 -0
  105. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/routing/paths.py +0 -0
  106. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/ClientOnly.jsx +0 -0
  107. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/Head.jsx +0 -0
  108. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/Image.jsx +0 -0
  109. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/Script.jsx +0 -0
  110. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime.py +0 -0
  111. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/__init__.py +0 -0
  112. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/_escape.py +0 -0
  113. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/head_merger.py +0 -0
  114. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/render_component.mjs +0 -0
  115. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/renderer.py +0 -0
  116. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/ssr_worker.mjs +0 -0
  117. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/template.py +0 -0
  118. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/worker_pool.py +0 -0
  119. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/__init__.py +0 -0
  120. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/.gitignore +0 -0
  121. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/__init__.py +0 -0
  122. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/package.json +0 -0
  123. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/api/pulse.py +0 -0
  124. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/index.pyxl +0 -0
  125. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/layout.pyxl +0 -0
  126. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/styles/tailwind.css +0 -0
  127. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/postcss.config.cjs +0 -0
  128. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/public/branding/pyxle-mark.svg +0 -0
  129. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/public/styles/tailwind.css +0 -0
  130. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pyxle.config.json +0 -0
  131. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/requirements.txt +0 -0
  132. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/tailwind.config.cjs +0 -0
  133. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/__init__.py +0 -0
  134. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/__init__.py +0 -0
  135. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_manifest.py +0 -0
  136. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_pipeline.py +0 -0
  137. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_pipeline_css.py +0 -0
  138. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_vite.py +0 -0
  139. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/__init__.py +0 -0
  140. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_commands.py +0 -0
  141. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_logger.py +0 -0
  142. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_scaffold.py +0 -0
  143. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_templates.py +0 -0
  144. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/__init__.py +0 -0
  145. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_compile.py +0 -0
  146. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_model.py +0 -0
  147. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_parser.py +0 -0
  148. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_writers.py +0 -0
  149. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_compile.py +0 -0
  150. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_head_jsx.py +0 -0
  151. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_jsx_imports.py +0 -0
  152. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_jsx_parser.py +0 -0
  153. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_parser.py +0 -0
  154. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_parser_diagnostics.py +0 -0
  155. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_parser_hardening.py +0 -0
  156. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_script_image_detection.py +0 -0
  157. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_script_image_integration.py +0 -0
  158. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/__init__.py +0 -0
  159. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/sample_middlewares.py +0 -0
  160. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_action_routes.py +0 -0
  161. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_build.py +0 -0
  162. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_builder.py +0 -0
  163. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_client_only.py +0 -0
  164. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_csrf.py +0 -0
  165. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_devserver_start.py +0 -0
  166. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_error_pages.py +0 -0
  167. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_layouts.py +0 -0
  168. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_middleware.py +0 -0
  169. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_overlay.py +0 -0
  170. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_proxy.py +0 -0
  171. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_route_error_boundary.py +0 -0
  172. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_route_hooks.py +0 -0
  173. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_routes.py +0 -0
  174. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_scanner.py +0 -0
  175. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_scripts.py +0 -0
  176. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_security_utils.py +0 -0
  177. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_settings.py +0 -0
  178. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_starlette_app.py +0 -0
  179. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_styles.py +0 -0
  180. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_tailwind.py +0 -0
  181. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_vite.py +0 -0
  182. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_watcher.py +0 -0
  183. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/__init__.py +0 -0
  184. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_dynamic_head.py +0 -0
  185. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_escape.py +0 -0
  186. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_head_merger_extra.py +0 -0
  187. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_head_merging.py +0 -0
  188. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_integration.py +0 -0
  189. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_renderer.py +0 -0
  190. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_script_injection.py +0 -0
  191. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_template.py +0 -0
  192. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_view.py +0 -0
  193. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_view_error_boundaries.py +0 -0
  194. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_worker_pool.py +0 -0
  195. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/utils.py +0 -0
  196. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_config.py +0 -0
  197. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_config_security.py +0 -0
  198. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_env.py +0 -0
  199. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_runtime.py +0 -0
  200. {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_runtime_errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyxle-framework
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python-first full-stack framework with an intelligent CLI.
5
5
  Project-URL: Homepage, https://pyxle.dev
6
6
  Project-URL: Repository, https://github.com/pyxle-framework/pyxle
@@ -2,7 +2,7 @@
2
2
 
3
3
  Pyxle is a Python-first full-stack web framework that brings the Next.js developer experience to the Python ecosystem. Write server logic in Python, UI in React, and ship them together in `.pyxl` files.
4
4
 
5
- **Current version:** 0.1.0 (beta)
5
+ **Current version:** 0.2.2 (beta)
6
6
 
7
7
  ---
8
8
 
@@ -22,7 +22,7 @@ Django and Flask are backend frameworks that render templates. Pyxle is a full-s
22
22
 
23
23
  ### Is Pyxle production-ready?
24
24
 
25
- Pyxle is in **beta** (version 0.1.0). The core features are implemented and tested (940+ tests, 95%+ coverage), but the API may change before 1.0. Use it for new projects and experiments, but be prepared for breaking changes.
25
+ Pyxle is in **beta** (version 0.2.2). The core features are implemented and tested (1100+ tests, 95%+ coverage), but the API may change before 1.0. Use it for new projects and experiments, but be prepared for breaking changes.
26
26
 
27
27
  ### What Python version do I need?
28
28
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyxle-framework"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Python-first full-stack framework with an intelligent CLI."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -32,8 +32,11 @@ def load_manifest(path: Path | str) -> Dict[str, Any]:
32
32
  def _validate_asset_paths(data: Dict[str, Any], manifest_path: Path) -> None:
33
33
  """Ensure no asset file path escapes the expected build directory.
34
34
 
35
- Rejects paths containing ``..`` or starting with ``/`` to prevent a
36
- compromised Vite build from referencing arbitrary filesystem locations.
35
+ Rejects paths containing ``../`` (path traversal) or starting with ``/``
36
+ to prevent a compromised Vite build from referencing arbitrary filesystem
37
+ locations. Note: we check for ``../`` rather than ``..`` because Vite
38
+ may produce filenames like ``__...slug__-hash.js`` for catch-all routes,
39
+ which legitimately contain ``..`` as a substring.
37
40
  """
38
41
  for route_key, entry in data.items():
39
42
  if not isinstance(entry, dict):
@@ -52,7 +55,7 @@ def _validate_asset_paths(data: Dict[str, Any], manifest_path: Path) -> None:
52
55
  def _check_safe_path(
53
56
  value: str, route_key: str, field: str, manifest_path: Path
54
57
  ) -> None:
55
- if ".." in value or value.startswith("/"):
58
+ if "/../" in value or value.startswith("../") or value.startswith("/"):
56
59
  raise ValueError(
57
60
  f"Manifest {field} entry for '{route_key}' in "
58
61
  f"'{manifest_path}' contains unsafe path: '{value}'"
@@ -661,7 +661,8 @@ def serve(
661
661
  public_dir = resolved_dist / "public"
662
662
  if not public_dir.exists():
663
663
  logger.warning(
664
- f"Public assets directory '{public_dir}' does not exist; falling back to '{settings.public_dir}'."
664
+ f"Public assets directory '{public_dir}' does not exist did you run 'pyxle build' first? "
665
+ f"Falling back to source directory '{settings.public_dir}'."
665
666
  )
666
667
  public_static_dir = settings.public_dir
667
668
  else:
@@ -12,6 +12,7 @@ export { Script } from './Script.jsx';
12
12
  export { Image } from './Image.jsx';
13
13
  export { ClientOnly } from './ClientOnly.jsx';
14
14
  export { useAction } from './useAction.jsx';
15
+ export { usePathname } from './usePathname.jsx';
15
16
  export { Form } from './Form.jsx';
16
17
 
17
18
  // Re-export Link and navigation from existing runtime
@@ -0,0 +1,35 @@
1
+ /**
2
+ * usePathname — reactively track the current URL pathname.
3
+ *
4
+ * Usage:
5
+ * const pathname = usePathname();
6
+ * // Re-renders whenever Pyxle performs a client-side navigation.
7
+ *
8
+ * Returns window.location.pathname on the client. During SSR returns '/'.
9
+ */
10
+
11
+ import { useState, useEffect } from 'react';
12
+
13
+ export function usePathname() {
14
+ const [pathname, setPathname] = useState(
15
+ typeof window !== 'undefined' ? window.location.pathname : '/'
16
+ );
17
+
18
+ useEffect(() => {
19
+ // Sync on mount in case the SSR value differs.
20
+ setPathname(window.location.pathname);
21
+
22
+ function onRouteChange() {
23
+ setPathname(window.location.pathname);
24
+ }
25
+
26
+ window.addEventListener('pyxle:routechange', onRouteChange);
27
+ window.addEventListener('popstate', onRouteChange);
28
+ return () => {
29
+ window.removeEventListener('pyxle:routechange', onRouteChange);
30
+ window.removeEventListener('popstate', onRouteChange);
31
+ };
32
+ }, []);
33
+
34
+ return pathname;
35
+ }
@@ -37,6 +37,7 @@ def write_client_bootstrap_files(settings: DevServerSettings) -> None:
37
37
  "pyxle/head.jsx": _render_head_component(),
38
38
  "pyxle/client-only.jsx": _render_client_only_component(),
39
39
  "pyxle/use-action.jsx": _render_use_action_component(),
40
+ "pyxle/use-pathname.jsx": _render_use_pathname_component(),
40
41
  "pyxle/form.jsx": _render_form_component(),
41
42
  "pyxle/client.js": _render_client_barrel(),
42
43
  "pyxle/index.d.ts": _render_client_runtime_index_types(),
@@ -46,6 +47,7 @@ def write_client_bootstrap_files(settings: DevServerSettings) -> None:
46
47
  "pyxle/image.d.ts": _render_image_component_types(),
47
48
  "pyxle/head.d.ts": _render_head_component_types(),
48
49
  "pyxle/client-only.d.ts": _render_client_only_component_types(),
50
+ "pyxle/use-pathname.d.ts": _render_use_pathname_component_types(),
49
51
  }
50
52
 
51
53
  for relative_path, contents in files.items():
@@ -1450,6 +1452,8 @@ def _render_client_entry(settings: DevServerSettings) -> str:
1450
1452
  window.history[method]({ pyxle: true, pagePath: nextPagePath }, '', `${url.pathname}${url.search}${url.hash}`);
1451
1453
  }
1452
1454
 
1455
+ window.dispatchEvent(new CustomEvent('pyxle:routechange'));
1456
+
1453
1457
  if (options.scroll !== 'preserve') {
1454
1458
  window.scrollTo(0, 0);
1455
1459
  }
@@ -1491,6 +1495,7 @@ def _render_client_entry(settings: DevServerSettings) -> str:
1491
1495
  '',
1492
1496
  `${url.pathname}${url.search}${url.hash}`,
1493
1497
  );
1498
+ window.dispatchEvent(new CustomEvent('pyxle:routechange'));
1494
1499
  return true;
1495
1500
  } catch (error) {
1496
1501
  if (!(error instanceof DOMException && error.name === 'AbortError')) {
@@ -2407,6 +2412,65 @@ def _render_form_component() -> str:
2407
2412
  )
2408
2413
 
2409
2414
 
2415
+ def _render_use_pathname_component() -> str:
2416
+ return (
2417
+ dedent(
2418
+ """
2419
+ import { useState, useEffect } from 'react';
2420
+
2421
+ /**
2422
+ * usePathname — reactively track the current URL pathname.
2423
+ *
2424
+ * Returns window.location.pathname on the client. Re-renders
2425
+ * the component whenever Pyxle performs a client-side navigation.
2426
+ * During SSR returns '/'.
2427
+ */
2428
+ export function usePathname() {
2429
+ const [pathname, setPathname] = useState(
2430
+ typeof window !== 'undefined' ? window.location.pathname : '/'
2431
+ );
2432
+
2433
+ useEffect(() => {
2434
+ // Sync on mount in case the SSR value differs.
2435
+ setPathname(window.location.pathname);
2436
+
2437
+ function onRouteChange() {
2438
+ setPathname(window.location.pathname);
2439
+ }
2440
+
2441
+ window.addEventListener('pyxle:routechange', onRouteChange);
2442
+ window.addEventListener('popstate', onRouteChange);
2443
+ return () => {
2444
+ window.removeEventListener('pyxle:routechange', onRouteChange);
2445
+ window.removeEventListener('popstate', onRouteChange);
2446
+ };
2447
+ }, []);
2448
+
2449
+ return pathname;
2450
+ }
2451
+ """
2452
+ ).strip()
2453
+ + "\n"
2454
+ )
2455
+
2456
+
2457
+ def _render_use_pathname_component_types() -> str:
2458
+ return (
2459
+ dedent(
2460
+ """
2461
+ /**
2462
+ * Reactively track the current URL pathname.
2463
+ *
2464
+ * Re-renders the component on every client-side navigation.
2465
+ * Returns `'/'` during SSR.
2466
+ */
2467
+ export declare function usePathname(): string;
2468
+ """
2469
+ ).strip()
2470
+ + "\n"
2471
+ )
2472
+
2473
+
2410
2474
  def _render_client_barrel() -> str:
2411
2475
  return (
2412
2476
  dedent(
@@ -2416,6 +2480,7 @@ def _render_client_barrel() -> str:
2416
2480
  export { Image } from './image.jsx';
2417
2481
  export { default as ClientOnly } from './client-only.jsx';
2418
2482
  export { useAction } from './use-action.jsx';
2483
+ export { usePathname } from './use-pathname.jsx';
2419
2484
  export { Form } from './form.jsx';
2420
2485
  export { Link, navigate, prefetch, refresh, Slot, SlotProvider, useSlot, useSlots } from './index.js';
2421
2486
  """
@@ -2439,5 +2504,7 @@ __all__ = [
2439
2504
  "_render_slot_runtime_types",
2440
2505
  "_render_tsconfig",
2441
2506
  "_render_vite_config",
2507
+ "_render_use_pathname_component",
2508
+ "_render_use_pathname_component_types",
2442
2509
  "_build_public_env_defines",
2443
2510
  ]
@@ -379,3 +379,62 @@ def find_layout_head_jsx_blocks(
379
379
  layout_head_blocks.extend(metadata.head_elements)
380
380
 
381
381
  return tuple(layout_head_blocks)
382
+
383
+
384
+ @dataclass(frozen=True, slots=True)
385
+ class LayoutLoaderInfo:
386
+ """Metadata needed to execute a layout's ``@server`` loader."""
387
+
388
+ relative_path: Path
389
+ server_module_path: Path
390
+ module_key: str
391
+ loader_name: str
392
+
393
+
394
+ def find_layout_loaders(
395
+ settings: DevServerSettings,
396
+ page_relative_path: Path,
397
+ ) -> tuple[LayoutLoaderInfo, ...]:
398
+ """Discover layout/template files with ``@server`` loaders that wrap *page_relative_path*.
399
+
400
+ Walks ancestor directories from the page's location (closest first, root last)
401
+ and returns a :class:`LayoutLoaderInfo` for each layout or template whose
402
+ compiled metadata declares a loader. The order matches the wrapping order
403
+ used by :func:`find_layout_head_jsx_blocks`.
404
+ """
405
+
406
+ parts = list(page_relative_path.parent.parts)
407
+ ancestors: List[Path] = []
408
+
409
+ if page_relative_path.parent.name:
410
+ ancestors.append(page_relative_path.parent)
411
+
412
+ for index in range(len(parts) - 1, 0, -1):
413
+ ancestors.append(Path(*parts[:index]))
414
+
415
+ ancestors.append(Path("."))
416
+
417
+ loaders: List[LayoutLoaderInfo] = []
418
+
419
+ for ancestor_dir in ancestors:
420
+ for filename in ("layout.pyxl", "template.pyxl"):
421
+ relative = ancestor_dir / filename if ancestor_dir != Path(".") else Path(filename)
422
+ metadata_path = settings.metadata_build_dir / "pages" / relative.with_suffix(".json")
423
+ if ancestor_dir == Path("."):
424
+ metadata_path = settings.metadata_build_dir / "pages" / Path(filename).with_suffix(".json")
425
+
426
+ metadata = _load_page_metadata(metadata_path)
427
+ if metadata is None or not metadata.loader_name:
428
+ continue
429
+
430
+ server_module = settings.server_build_dir / "pages" / relative.with_suffix(".py")
431
+ module_key = relative.with_suffix("").as_posix().replace("/", ".")
432
+
433
+ loaders.append(LayoutLoaderInfo(
434
+ relative_path=relative,
435
+ server_module_path=server_module,
436
+ module_key=module_key,
437
+ loader_name=metadata.loader_name,
438
+ ))
439
+
440
+ return tuple(loaders)
@@ -364,6 +364,43 @@ async def _execute_loader(
364
364
  return payload, status_code, module
365
365
 
366
366
 
367
+ async def _execute_layout_loaders(
368
+ *,
369
+ settings: DevServerSettings,
370
+ page: PageRoute,
371
+ request: Request,
372
+ ) -> dict[str, Any] | None:
373
+ """Execute ``@server`` loaders declared in ancestor layout/template files.
374
+
375
+ Returns a dict of loader results (one entry per layout that has a loader),
376
+ or ``None`` if no layout declares a loader.
377
+ """
378
+ from pyxle.devserver.registry import find_layout_loaders
379
+
380
+ layout_loader_infos = find_layout_loaders(settings, page.source_relative_path)
381
+ if not layout_loader_infos:
382
+ return None
383
+
384
+ layout_data: dict[str, Any] = {}
385
+ for info in layout_loader_infos:
386
+ module = _import_server_module(info.module_key, info.server_module_path, debug=settings.debug)
387
+ loader_fn = getattr(module, info.loader_name, None)
388
+ if loader_fn is None:
389
+ continue
390
+
391
+ result = loader_fn(request)
392
+ if hasattr(result, "__await__"):
393
+ result = await result
394
+
395
+ # Layout loaders return a plain dict (no status code).
396
+ if isinstance(result, tuple) and result:
397
+ result = result[0]
398
+ if isinstance(result, Mapping):
399
+ layout_data.update(result)
400
+
401
+ return layout_data or None
402
+
403
+
367
404
  def _resolve_head_elements(
368
405
  page: PageRoute,
369
406
  module,
@@ -404,8 +441,14 @@ def _normalize_loader_result(result: Any, page: PageRoute) -> Tuple[dict[str, An
404
441
  return dict(payload), status_code
405
442
 
406
443
 
407
- def _compose_component_props(loader_payload: dict[str, Any]) -> dict[str, Any]:
408
- return {"data": loader_payload}
444
+ def _compose_component_props(
445
+ loader_payload: dict[str, Any],
446
+ layout_data: dict[str, Any] | None = None,
447
+ ) -> dict[str, Any]:
448
+ props: dict[str, Any] = {"data": loader_payload}
449
+ if layout_data:
450
+ props["layoutData"] = layout_data
451
+ return props
409
452
 
410
453
 
411
454
  async def _create_page_artifacts(
@@ -434,14 +477,21 @@ async def _create_page_artifacts(
434
477
  loader_breadcrumb["detail"] = f"Returned {len(loader_props)} key(s) with status {status_code}"
435
478
 
436
479
  head_elements = _resolve_head_elements(page, module, loader_props, debug=settings.debug)
437
-
480
+
438
481
  # Merge HEAD variable with JSX Head blocks and layout head blocks
439
- from pyxle.devserver.registry import find_layout_head_jsx_blocks
482
+ from pyxle.devserver.registry import find_layout_head_jsx_blocks, find_layout_loaders
440
483
  from pyxle.ssr.head_merger import merge_head_elements
441
-
484
+
442
485
  layout_head_jsx_blocks = find_layout_head_jsx_blocks(settings, page.source_relative_path)
443
-
444
- component_props = _compose_component_props(loader_props)
486
+
487
+ # Execute layout loaders (if any layout has a @server decorator)
488
+ layout_data = await _execute_layout_loaders(
489
+ settings=settings,
490
+ page=page,
491
+ request=request,
492
+ )
493
+
494
+ component_props = _compose_component_props(loader_props, layout_data)
445
495
  render_result = await renderer.render(page.client_module_path, component_props)
446
496
  body_html = render_result.html
447
497
  inline_styles = render_result.inline_styles
@@ -15,6 +15,8 @@ from pyxle.devserver.client_files import (
15
15
  _render_slot_runtime,
16
16
  _render_slot_runtime_types,
17
17
  _render_tsconfig,
18
+ _render_use_pathname_component,
19
+ _render_use_pathname_component_types,
18
20
  _render_vite_config,
19
21
  write_client_bootstrap_files,
20
22
  )
@@ -335,3 +337,48 @@ def test_client_entry_includes_bfcache_pageshow_handler(tmp_path: Path) -> None:
335
337
  assert "addEventListener('pageshow'" in entry or 'addEventListener("pageshow"' in entry
336
338
  assert "event.persisted" in entry
337
339
  assert "router.refresh()" in entry
340
+
341
+
342
+ # ---------------------------------------------------------------------------
343
+ # usePathname hook
344
+ # ---------------------------------------------------------------------------
345
+
346
+
347
+ def test_client_entry_dispatches_route_change_event(tmp_path: Path) -> None:
348
+ """The client runtime dispatches a ``pyxle:routechange`` custom event
349
+ after both ``navigateTo`` and ``refreshCurrentPage`` complete. This
350
+ is the signal consumed by ``usePathname()``."""
351
+ settings = create_project(tmp_path)
352
+ entry = _render_client_entry(settings)
353
+
354
+ assert "pyxle:routechange" in entry
355
+ # Must appear at least twice: once in navigateTo, once in refreshCurrentPage.
356
+ assert entry.count("pyxle:routechange") >= 2
357
+
358
+
359
+ def test_use_pathname_component_is_ssr_safe() -> None:
360
+ """The generated usePathname hook must guard window access for SSR."""
361
+ source = _render_use_pathname_component()
362
+ assert "typeof window" in source
363
+ assert "usePathname" in source
364
+ assert "pyxle:routechange" in source
365
+
366
+
367
+ def test_use_pathname_component_types() -> None:
368
+ """Type definition declares usePathname returning a string."""
369
+ types = _render_use_pathname_component_types()
370
+ assert "usePathname" in types
371
+ assert "string" in types
372
+
373
+
374
+ def test_write_client_bootstrap_files_generates_use_pathname(tmp_path: Path) -> None:
375
+ """Bootstrap writes both the JSX hook and its type declaration."""
376
+ settings = create_project(tmp_path)
377
+ write_client_bootstrap_files(settings)
378
+
379
+ hook = (settings.client_build_dir / "pyxle" / "use-pathname.jsx").read_text(encoding="utf-8")
380
+ assert "usePathname" in hook
381
+ assert "pyxle:routechange" in hook
382
+
383
+ types = (settings.client_build_dir / "pyxle" / "use-pathname.d.ts").read_text(encoding="utf-8")
384
+ assert "usePathname" in types
@@ -278,3 +278,68 @@ def test_find_layout_head_jsx_blocks_layout_hierarchy(project: DevServerSettings
278
278
  assert "root" in all_blocks
279
279
  assert "posts" in all_blocks
280
280
 
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # find_layout_loaders
284
+ # ---------------------------------------------------------------------------
285
+
286
+
287
+ def test_find_layout_loaders_no_layout(project: DevServerSettings) -> None:
288
+ """Returns empty tuple when no layout file exists."""
289
+ from pyxle.devserver.registry import find_layout_loaders
290
+
291
+ build_once(project)
292
+ loaders = find_layout_loaders(project, Path("index.pyxl"))
293
+ assert loaders == ()
294
+
295
+
296
+ def test_find_layout_loaders_layout_without_loader(project: DevServerSettings) -> None:
297
+ """Layout file with no @server decorator yields no loader info."""
298
+ from pyxle.devserver.registry import find_layout_loaders
299
+
300
+ write_file(
301
+ project.pages_dir / "layout.pyxl",
302
+ "import React from 'react';\n\nexport default function Layout({ children }) {\n return <div>{children}</div>;\n}\n",
303
+ )
304
+
305
+ build_once(project)
306
+ loaders = find_layout_loaders(project, Path("index.pyxl"))
307
+ assert loaders == ()
308
+
309
+
310
+ def test_find_layout_loaders_layout_with_loader(project: DevServerSettings) -> None:
311
+ """Layout file with @server decorator is discovered."""
312
+ from pyxle.devserver.registry import find_layout_loaders
313
+
314
+ write_file(
315
+ project.pages_dir / "layout.pyxl",
316
+ "@server\nasync def load_layout(request):\n return {'app': 'test'}\n\nimport React from 'react';\n\nexport default function Layout({ children }) {\n return <div>{children}</div>;\n}\n",
317
+ )
318
+
319
+ build_once(project)
320
+ loaders = find_layout_loaders(project, Path("index.pyxl"))
321
+ assert len(loaders) == 1
322
+ assert loaders[0].loader_name == "load_layout"
323
+ assert loaders[0].server_module_path.exists()
324
+
325
+
326
+ def test_find_layout_loaders_nested_hierarchy(project: DevServerSettings) -> None:
327
+ """Both root and nested layout loaders are discovered in order."""
328
+ from pyxle.devserver.registry import find_layout_loaders
329
+
330
+ write_file(
331
+ project.pages_dir / "layout.pyxl",
332
+ "@server\nasync def load_root(request):\n return {'level': 'root'}\n\nimport React from 'react';\n\nexport default function RootLayout({ children }) {\n return <html>{children}</html>;\n}\n",
333
+ )
334
+ write_file(
335
+ project.pages_dir / "posts" / "layout.pyxl",
336
+ "@server\nasync def load_posts(request):\n return {'level': 'posts'}\n\nimport React from 'react';\n\nexport default function PostsLayout({ children }) {\n return <section>{children}</section>;\n}\n",
337
+ )
338
+
339
+ build_once(project)
340
+ loaders = find_layout_loaders(project, Path("posts/[id].pyxl"))
341
+ assert len(loaders) == 2
342
+ # Closest layout first
343
+ assert loaders[0].loader_name == "load_posts"
344
+ assert loaders[1].loader_name == "load_root"
345
+
File without changes