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.
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/PKG-INFO +1 -1
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/README.md +1 -1
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/faq.md +1 -1
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyproject.toml +1 -1
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/manifest.py +6 -3
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/__init__.py +2 -1
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/index.js +1 -0
- pyxle_framework-0.2.2/pyxle/client/usePathname.jsx +35 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/client_files.py +67 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/registry.py +59 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/view.py +57 -7
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_client_files.py +47 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_registry.py +65 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/.github/pyxle-logo.svg +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/.github/workflows/publish.yml +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/.gitignore +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/CLAUDE.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/LICENSE +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/Makefile +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/README.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/advanced/compiler-internals.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/advanced/ssr-pipeline.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/README.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/build-and-serve.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/cli.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/compiler.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/dev-server.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/overview.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/parser.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/pyxl-files.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/routing.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/runtime.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/architecture/ssr.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/data-loading.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/layouts.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/pyxl-files.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/routing.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/core-concepts/server-actions.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/getting-started/installation.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/getting-started/project-structure.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/getting-started/quick-start.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/api-routes.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/client-components.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/deployment.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/editor-setup.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/environment-variables.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/error-handling.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/for-ai-agents.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/head-management.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/middleware.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/migration-pyx-to-pyxl.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/security.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/guides/styling.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/cli.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/client-api.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/configuration.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/docs/reference/runtime-api.md +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/pipeline.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/build/vite.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/assets.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/init.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/logger.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/scaffold.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/cli/templates.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/ClientOnly.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Form.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Head.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Image.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/Script.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/client/useAction.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/core.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/exceptions.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/jsx_imports.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/jsx_parser.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/model.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/parser.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/compiler/writers.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/config.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/_security.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/build.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/builder.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/csrf.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/error_pages.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/layouts.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/middleware.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/overlay.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/path_utils.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/proxy.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/route_hooks.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/routes.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/scanner.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/scripts.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/settings.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/starlette_app.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/styles.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/tailwind.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/vite.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/devserver/watcher.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/env.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/routing/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/routing/paths.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/ClientOnly.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/Head.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/Image.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime/Script.jsx +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/runtime.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/_escape.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/head_merger.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/render_component.mjs +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/renderer.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/ssr_worker.mjs +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/template.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/ssr/worker_pool.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/.gitignore +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/package.json +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/api/pulse.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/index.pyxl +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/layout.pyxl +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pages/styles/tailwind.css +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/postcss.config.cjs +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/public/branding/pyxle-mark.svg +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/public/styles/tailwind.css +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/pyxle.config.json +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/requirements.txt +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/pyxle/templates/scaffold/tailwind.config.cjs +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_manifest.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_pipeline.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_pipeline_css.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/build/test_vite.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_commands.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_logger.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_scaffold.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/cli/test_templates.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_compile.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_model.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_parser.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_action_writers.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_compile.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_head_jsx.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_jsx_imports.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_jsx_parser.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_parser.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_parser_diagnostics.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_parser_hardening.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_script_image_detection.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/compiler/test_script_image_integration.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/sample_middlewares.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_action_routes.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_build.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_builder.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_client_only.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_csrf.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_devserver_start.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_error_pages.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_layouts.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_middleware.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_overlay.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_proxy.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_route_error_boundary.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_route_hooks.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_routes.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_scanner.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_scripts.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_security_utils.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_settings.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_starlette_app.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_styles.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_tailwind.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_vite.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/devserver/test_watcher.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/__init__.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_dynamic_head.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_escape.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_head_merger_extra.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_head_merging.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_integration.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_renderer.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_script_injection.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_template.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_view.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_view_error_boundaries.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/test_worker_pool.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/ssr/utils.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_config.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_config_security.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_env.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_runtime.py +0 -0
- {pyxle_framework-0.2.0 → pyxle_framework-0.2.2}/tests/test_runtime_errors.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
36
|
-
compromised Vite build from referencing arbitrary filesystem
|
|
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 "
|
|
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
|
|
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(
|
|
408
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|