pyxle-framework 0.2.2__tar.gz → 0.2.4__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.2 → pyxle_framework-0.2.4}/PKG-INFO +1 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/README.md +1 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/faq.md +6 -2
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/getting-started/project-structure.md +17 -7
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/getting-started/quick-start.md +13 -21
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/deployment.md +1 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/middleware.md +8 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/reference/client-api.md +32 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyproject.toml +1 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/cli/init.py +2 -4
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/client/Form.jsx +4 -0
- pyxle_framework-0.2.4/pyxle/client/Head.jsx +122 -0
- pyxle_framework-0.2.4/pyxle/client/Image.jsx +134 -0
- pyxle_framework-0.2.4/pyxle/client/Script.jsx +190 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/client/useAction.jsx +5 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/client/usePathname.jsx +17 -4
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/client_files.py +412 -86
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/_escape.py +15 -8
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/render_component.mjs +25 -6
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/renderer.py +89 -11
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/ssr_worker.mjs +23 -6
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/view.py +6 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/worker_pool.py +9 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_client_files.py +185 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_escape.py +34 -4
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_renderer.py +51 -1
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_view.py +26 -3
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_worker_pool.py +58 -0
- pyxle_framework-0.2.2/pyxle/client/Head.jsx +0 -34
- pyxle_framework-0.2.2/pyxle/client/Image.jsx +0 -27
- pyxle_framework-0.2.2/pyxle/client/Script.jsx +0 -19
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/.github/pyxle-logo.svg +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/.github/workflows/publish.yml +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/.gitignore +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/CLAUDE.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/LICENSE +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/Makefile +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/README.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/advanced/compiler-internals.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/advanced/ssr-pipeline.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/README.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/build-and-serve.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/cli.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/compiler.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/dev-server.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/overview.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/parser.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/pyxl-files.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/routing.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/runtime.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/architecture/ssr.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/core-concepts/data-loading.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/core-concepts/layouts.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/core-concepts/pyxl-files.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/core-concepts/routing.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/core-concepts/server-actions.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/getting-started/installation.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/api-routes.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/client-components.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/editor-setup.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/environment-variables.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/error-handling.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/for-ai-agents.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/head-management.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/migration-pyx-to-pyxl.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/security.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/guides/styling.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/reference/cli.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/reference/configuration.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/docs/reference/runtime-api.md +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/build/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/build/manifest.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/build/pipeline.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/build/vite.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/cli/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/cli/assets.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/cli/logger.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/cli/scaffold.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/cli/templates.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/client/ClientOnly.jsx +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/client/index.js +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/core.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/exceptions.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/jsx_imports.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/jsx_parser.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/model.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/parser.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/compiler/writers.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/config.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/_security.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/build.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/builder.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/csrf.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/error_pages.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/layouts.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/middleware.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/overlay.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/path_utils.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/proxy.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/registry.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/route_hooks.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/routes.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/scanner.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/scripts.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/settings.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/starlette_app.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/styles.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/tailwind.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/vite.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/devserver/watcher.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/env.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/routing/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/routing/paths.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/runtime/ClientOnly.jsx +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/runtime/Head.jsx +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/runtime/Image.jsx +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/runtime/Script.jsx +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/runtime.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/head_merger.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/ssr/template.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/.gitignore +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/package.json +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/pages/api/pulse.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/pages/index.pyxl +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/pages/layout.pyxl +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/pages/styles/tailwind.css +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/postcss.config.cjs +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/public/branding/pyxle-mark.svg +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/public/styles/tailwind.css +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/pyxle.config.json +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/requirements.txt +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/pyxle/templates/scaffold/tailwind.config.cjs +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/build/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/build/test_manifest.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/build/test_pipeline.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/build/test_pipeline_css.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/build/test_vite.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/cli/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/cli/test_commands.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/cli/test_logger.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/cli/test_scaffold.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/cli/test_templates.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_action_compile.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_action_model.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_action_parser.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_action_writers.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_compile.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_head_jsx.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_jsx_imports.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_jsx_parser.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_parser.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_parser_diagnostics.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_parser_hardening.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_script_image_detection.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/compiler/test_script_image_integration.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/sample_middlewares.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_action_routes.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_build.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_builder.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_client_only.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_csrf.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_devserver_start.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_error_pages.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_layouts.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_middleware.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_overlay.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_proxy.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_registry.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_route_error_boundary.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_route_hooks.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_routes.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_scanner.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_scripts.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_security_utils.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_settings.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_starlette_app.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_styles.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_tailwind.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_vite.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/devserver/test_watcher.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/__init__.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_dynamic_head.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_head_merger_extra.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_head_merging.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_integration.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_script_injection.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_template.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/test_view_error_boundaries.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/ssr/utils.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/test_config.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/test_config_security.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/test_env.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/tests/test_runtime.py +0 -0
- {pyxle_framework-0.2.2 → pyxle_framework-0.2.4}/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.2.
|
|
5
|
+
**Current version:** 0.2.4 (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.2.
|
|
25
|
+
Pyxle is in **beta** (version 0.2.4). 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
|
|
|
@@ -193,7 +193,11 @@ Run `pyxle install` or `npm install` in your project directory.
|
|
|
193
193
|
|
|
194
194
|
### The page renders without styles
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
`pyxle dev` compiles Tailwind through PostCSS automatically when
|
|
197
|
+
`postcss.config.cjs` is present (the scaffold default). If you removed
|
|
198
|
+
that file or are on a custom stylesheet setup, re-check your
|
|
199
|
+
[styling configuration](guides/styling.md) — specifically that the
|
|
200
|
+
relevant CSS is imported from a page or layout.
|
|
197
201
|
|
|
198
202
|
### Hot reload is not working
|
|
199
203
|
|
|
@@ -75,11 +75,17 @@ A `.pyxl` file combines Python server logic with a React component. The scaffold
|
|
|
75
75
|
|
|
76
76
|
```python
|
|
77
77
|
# Python section
|
|
78
|
+
from datetime import datetime, timezone
|
|
78
79
|
from pyxle import __version__
|
|
79
80
|
|
|
80
81
|
@server
|
|
81
82
|
async def load_home(request):
|
|
82
|
-
|
|
83
|
+
now = datetime.now(tz=timezone.utc)
|
|
84
|
+
return {
|
|
85
|
+
"version": __version__,
|
|
86
|
+
"time": now.strftime("%H:%M:%S UTC"),
|
|
87
|
+
"message": "You're ready to build with Pyxle.",
|
|
88
|
+
}
|
|
83
89
|
```
|
|
84
90
|
|
|
85
91
|
```jsx
|
|
@@ -88,12 +94,13 @@ import { Head } from 'pyxle/client';
|
|
|
88
94
|
|
|
89
95
|
export default function HomePage({ data }) {
|
|
90
96
|
return (
|
|
91
|
-
|
|
97
|
+
<main>
|
|
92
98
|
<Head>
|
|
93
|
-
<title>
|
|
99
|
+
<title>Pyxle App</title>
|
|
94
100
|
</Head>
|
|
95
101
|
<h1>{data.message}</h1>
|
|
96
|
-
|
|
102
|
+
<p>Pyxle v{data.version} · {data.time}</p>
|
|
103
|
+
</main>
|
|
97
104
|
);
|
|
98
105
|
}
|
|
99
106
|
```
|
|
@@ -143,9 +150,12 @@ Defines Node.js dependencies and npm scripts:
|
|
|
143
150
|
| Script | Purpose |
|
|
144
151
|
|--------|---------|
|
|
145
152
|
| `npm run dev` | Start Vite dev server (used internally by `pyxle dev`) |
|
|
146
|
-
| `npm run build` |
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
| `npm run build` | Bundle with Vite (used by `pyxle build`); Tailwind is compiled in-pipeline via PostCSS |
|
|
154
|
+
|
|
155
|
+
Tailwind is wired through PostCSS (see `postcss.config.cjs`), so Vite
|
|
156
|
+
handles CSS on both `dev` and `build` without a separate script. If you
|
|
157
|
+
want a standalone Tailwind watcher anyway, see
|
|
158
|
+
[Styling guide → Standalone Tailwind](../guides/styling.md).
|
|
149
159
|
|
|
150
160
|
### `tailwind.config.cjs`
|
|
151
161
|
|
|
@@ -24,23 +24,15 @@ pip install -r requirements.txt
|
|
|
24
24
|
npm install
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
## 3.
|
|
28
|
-
|
|
29
|
-
In a separate terminal, start the Tailwind watcher:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npm run dev:css
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
This compiles `pages/styles/tailwind.css` into `public/styles/tailwind.css` and watches for changes. The dev server also auto-starts Tailwind if it detects a config file, so this step is optional if you use `pyxle dev` with the `--tailwind` flag (enabled by default).
|
|
36
|
-
|
|
37
|
-
## 4. Start the dev server
|
|
27
|
+
## 3. Start the dev server
|
|
38
28
|
|
|
39
29
|
```bash
|
|
40
30
|
pyxle dev
|
|
41
31
|
```
|
|
42
32
|
|
|
43
|
-
Open [http://localhost:8000](http://localhost:8000) in your browser. You should see the Pyxle
|
|
33
|
+
Open [http://localhost:8000](http://localhost:8000) in your browser. You should see the Pyxle starter page — a centered card showing the framework version, server time, and a link to edit `pages/index.pyxl`.
|
|
34
|
+
|
|
35
|
+
Tailwind compiles automatically because the scaffold ships with `postcss.config.cjs` — PostCSS runs as part of the Vite pipeline, so there's nothing separate to start.
|
|
44
36
|
|
|
45
37
|
## What just happened?
|
|
46
38
|
|
|
@@ -53,24 +45,24 @@ When you ran `pyxle dev`, the framework:
|
|
|
53
45
|
5. **Rendered HTML on the server** -- sent fully-rendered HTML to the browser (SSR)
|
|
54
46
|
6. **Hydrated on the client** -- React took over the server-rendered HTML for interactivity
|
|
55
47
|
|
|
56
|
-
##
|
|
48
|
+
## 4. Make a change
|
|
57
49
|
|
|
58
|
-
Open `pages/index.pyxl` in your editor.
|
|
50
|
+
Open `pages/index.pyxl` in your editor. Change the `message` returned by `load_home`:
|
|
59
51
|
|
|
60
52
|
```python
|
|
61
53
|
@server
|
|
62
54
|
async def load_home(request):
|
|
55
|
+
now = datetime.now(tz=timezone.utc)
|
|
63
56
|
return {
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
57
|
+
"version": __version__,
|
|
58
|
+
"time": now.strftime("%H:%M:%S UTC"),
|
|
59
|
+
"message": "Hello from my Pyxle app!",
|
|
68
60
|
}
|
|
69
61
|
```
|
|
70
62
|
|
|
71
|
-
Save the file. The browser reloads automatically with your updated
|
|
63
|
+
Save the file. The browser reloads automatically with your updated message.
|
|
72
64
|
|
|
73
|
-
##
|
|
65
|
+
## 5. Check your routes
|
|
74
66
|
|
|
75
67
|
```bash
|
|
76
68
|
pyxle routes
|
|
@@ -84,7 +76,7 @@ Route File Loader
|
|
|
84
76
|
/api/pulse pages/api/pulse.py --
|
|
85
77
|
```
|
|
86
78
|
|
|
87
|
-
##
|
|
79
|
+
## 6. Validate your project
|
|
88
80
|
|
|
89
81
|
```bash
|
|
90
82
|
pyxle check
|
|
@@ -11,7 +11,7 @@ pyxle build
|
|
|
11
11
|
This:
|
|
12
12
|
|
|
13
13
|
1. Compiles all `.pyxl` files into Python and JSX modules
|
|
14
|
-
2. Runs `npm run build` (
|
|
14
|
+
2. Runs `npm run build` — Vite bundles JS and processes CSS through PostCSS (Tailwind is compiled in-pipeline when `postcss.config.cjs` is present, which is the scaffold default)
|
|
15
15
|
3. Outputs production artifacts to the `dist/` directory
|
|
16
16
|
|
|
17
17
|
### Build options
|
|
@@ -98,10 +98,18 @@ The `context` object provides metadata about the matched route:
|
|
|
98
98
|
| `target` | `"page" \| "api"` | Route type |
|
|
99
99
|
| `path` | `str` | URL path pattern |
|
|
100
100
|
| `source_relative_path` | `Path` | File path relative to project root |
|
|
101
|
+
| `source_absolute_path` | `Path` | Absolute file path on disk |
|
|
101
102
|
| `module_key` | `str` | Python import key |
|
|
103
|
+
| `content_hash` | `str` | Hash of the compiled route module — changes when the source changes, stable across reloads |
|
|
102
104
|
| `has_loader` | `bool` | Whether the page has a `@server` loader |
|
|
105
|
+
| `head_elements` | `tuple[str, ...]` | Rendered `<head>` markup registered by the page/layout (SSR only) |
|
|
103
106
|
| `allowed_methods` | `tuple[str, ...]` | HTTP methods the route handles |
|
|
104
107
|
|
|
108
|
+
`RouteContext` is a frozen dataclass — fields are read-only. A shorthand
|
|
109
|
+
`context.as_dict()` returns a JSON-friendly view (keys camelCased, paths
|
|
110
|
+
as POSIX strings) that Pyxle attaches to `request.scope["pyxle"]["route"]`
|
|
111
|
+
for downstream middleware.
|
|
112
|
+
|
|
105
113
|
### Built-in hooks
|
|
106
114
|
|
|
107
115
|
Pyxle applies two default hooks:
|
|
@@ -6,7 +6,7 @@ All client-side components and hooks are importable from `pyxle/client`:
|
|
|
6
6
|
import {
|
|
7
7
|
Head, Script, Image, ClientOnly,
|
|
8
8
|
Form, useAction,
|
|
9
|
-
Link, navigate, prefetch, refresh
|
|
9
|
+
Link, navigate, prefetch, refresh, usePathname
|
|
10
10
|
} from 'pyxle/client';
|
|
11
11
|
```
|
|
12
12
|
|
|
@@ -204,6 +204,37 @@ const result = await actionFn(payload);
|
|
|
204
204
|
|
|
205
205
|
---
|
|
206
206
|
|
|
207
|
+
### `usePathname()`
|
|
208
|
+
|
|
209
|
+
Reactive hook that returns the current URL pathname and re-renders on
|
|
210
|
+
client-side navigation.
|
|
211
|
+
|
|
212
|
+
```jsx
|
|
213
|
+
import { usePathname, Link } from 'pyxle/client';
|
|
214
|
+
|
|
215
|
+
function NavLink({ href, children }) {
|
|
216
|
+
const pathname = usePathname();
|
|
217
|
+
const active = pathname === href;
|
|
218
|
+
return (
|
|
219
|
+
<Link href={href} className={active ? 'text-emerald-400' : 'text-zinc-400'}>
|
|
220
|
+
{children}
|
|
221
|
+
</Link>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Returns:** `string` — the current pathname (e.g. `/dashboard/settings`).
|
|
227
|
+
|
|
228
|
+
**Behaviour:**
|
|
229
|
+
- Reads `window.location.pathname` on the client
|
|
230
|
+
- During SSR, returns the path currently being rendered (via
|
|
231
|
+
`globalThis.__PYXLE_CURRENT_PATHNAME__`) so the first client render matches
|
|
232
|
+
— no hydration mismatch
|
|
233
|
+
- Subscribes to framework navigation events (`Link`, `navigate()`,
|
|
234
|
+
`refresh()`, `popstate`) and re-renders on change
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
207
238
|
## Functions
|
|
208
239
|
|
|
209
240
|
### `navigate(path)`
|
|
@@ -57,11 +57,9 @@ def log_next_steps(
|
|
|
57
57
|
if include_install_hint:
|
|
58
58
|
logger.info(" 2. pyxle install # installs Python + Node dependencies")
|
|
59
59
|
logger.info(" (or run 'pip install -r requirements.txt' and 'npm install')")
|
|
60
|
-
logger.info(" 3. npm run dev:css # watches Tailwind into public/styles/tailwind.css (separate terminal)")
|
|
61
|
-
logger.info(" 4. pyxle dev")
|
|
62
|
-
else:
|
|
63
|
-
logger.info(" 2. npm run dev:css # watches Tailwind into public/styles/tailwind.css (separate terminal)")
|
|
64
60
|
logger.info(" 3. pyxle dev")
|
|
61
|
+
else:
|
|
62
|
+
logger.info(" 2. pyxle dev")
|
|
65
63
|
|
|
66
64
|
|
|
67
65
|
def run_init(
|
|
@@ -34,6 +34,10 @@ function resolveActionUrl(actionName, pagePath) {
|
|
|
34
34
|
if (!page) {
|
|
35
35
|
if (typeof window !== 'undefined') {
|
|
36
36
|
page = window.location.pathname;
|
|
37
|
+
} else if (typeof globalThis.__PYXLE_CURRENT_PATHNAME__ === 'string') {
|
|
38
|
+
// SSR: use the framework-injected request path so the form's action
|
|
39
|
+
// URL matches the one the client will compute at hydration.
|
|
40
|
+
page = globalThis.__PYXLE_CURRENT_PATHNAME__;
|
|
37
41
|
} else {
|
|
38
42
|
page = '/';
|
|
39
43
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Head> — declare elements that belong in the document <head>.
|
|
3
|
+
*
|
|
4
|
+
* Works on both tiers of the render pipeline:
|
|
5
|
+
*
|
|
6
|
+
* • SSR — renders children to static markup and registers them
|
|
7
|
+
* with the framework's head registry, so they land in the
|
|
8
|
+
* initial HTML response.
|
|
9
|
+
*
|
|
10
|
+
* • Client — on mount, adopts the equivalent SSR-rendered elements
|
|
11
|
+
* (matched by tag + key attribute) so we don't duplicate
|
|
12
|
+
* them. On update, the adopted nodes are removed and
|
|
13
|
+
* fresh ones inserted, so state-driven head changes (e.g.
|
|
14
|
+
* a dynamic <title>) actually update the document.
|
|
15
|
+
* On unmount, everything this instance owns is removed and
|
|
16
|
+
* the previous <title> is restored.
|
|
17
|
+
*
|
|
18
|
+
* Multiple <Head> components compose — each one owns the nodes it rendered.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect } from 'react';
|
|
21
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
22
|
+
|
|
23
|
+
const OWNER_ATTR = 'data-pyxle-head-client';
|
|
24
|
+
// Key attributes that identify "the same" head element across renders.
|
|
25
|
+
// Order matters: we pick the first one that the declared element has.
|
|
26
|
+
const KEY_ATTRS = ['name', 'property', 'rel', 'href', 'src', 'charset', 'http-equiv'];
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function _findEquivalentHeadElement(target) {
|
|
30
|
+
const tag = target.tagName.toLowerCase();
|
|
31
|
+
const keyAttr = KEY_ATTRS.find((a) => target.hasAttribute(a));
|
|
32
|
+
if (!keyAttr) {
|
|
33
|
+
// Without a discriminating attribute we can't safely adopt — caller will
|
|
34
|
+
// insert a fresh copy instead.
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const keyValue = target.getAttribute(keyAttr);
|
|
39
|
+
const escape = (typeof CSS !== 'undefined' && CSS.escape) || ((s) => s);
|
|
40
|
+
const selector = `${tag}[${keyAttr}="${escape(keyValue)}"]:not([${OWNER_ATTR}])`;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return document.head.querySelector(selector);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
function _applyHeadMarkup(markup) {
|
|
51
|
+
if (!markup) return { nodes: [], previousTitle: null };
|
|
52
|
+
|
|
53
|
+
const template = document.createElement('template');
|
|
54
|
+
template.innerHTML = markup;
|
|
55
|
+
const parsed = Array.from(template.content.childNodes).filter(
|
|
56
|
+
(n) => n.nodeType === 1,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const nodes = [];
|
|
60
|
+
let previousTitle = null;
|
|
61
|
+
|
|
62
|
+
for (const declared of parsed) {
|
|
63
|
+
if (declared.tagName === 'TITLE') {
|
|
64
|
+
// Only one <title> wins; save the prior value so unmount can restore it.
|
|
65
|
+
if (previousTitle === null) previousTitle = document.title;
|
|
66
|
+
document.title = declared.textContent || '';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const existing = _findEquivalentHeadElement(declared);
|
|
71
|
+
if (existing) {
|
|
72
|
+
existing.setAttribute(OWNER_ATTR, '');
|
|
73
|
+
nodes.push(existing);
|
|
74
|
+
} else {
|
|
75
|
+
declared.setAttribute(OWNER_ATTR, '');
|
|
76
|
+
document.head.appendChild(declared);
|
|
77
|
+
nodes.push(declared);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { nodes, previousTitle };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export function Head({ children }) {
|
|
86
|
+
// Server-side: register markup with the framework's head registry so
|
|
87
|
+
// the template emits it into the initial HTML.
|
|
88
|
+
if (typeof window === 'undefined') {
|
|
89
|
+
if (typeof globalThis.__PYXLE_HEAD_REGISTRY__ !== 'undefined') {
|
|
90
|
+
try {
|
|
91
|
+
const headMarkup = renderToStaticMarkup(<>{children}</>);
|
|
92
|
+
globalThis.__PYXLE_HEAD_REGISTRY__.register(headMarkup);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('[Pyxle Head] SSR extraction failed:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Client-side: render children to a stable string so we can use it as
|
|
101
|
+
// the effect dependency (children itself changes identity every render).
|
|
102
|
+
let markup = '';
|
|
103
|
+
try {
|
|
104
|
+
markup = renderToStaticMarkup(<>{children}</>);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('[Pyxle Head] client render failed:', error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const { nodes, previousTitle } = _applyHeadMarkup(markup);
|
|
111
|
+
return () => {
|
|
112
|
+
for (const node of nodes) {
|
|
113
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
114
|
+
}
|
|
115
|
+
if (previousTitle !== null) {
|
|
116
|
+
document.title = previousTitle;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, [markup]);
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Image> — a thin wrapper around the native <img> with four useful
|
|
3
|
+
* behaviours on top:
|
|
4
|
+
*
|
|
5
|
+
* 1. Native lazy-loading via the standard `loading` attribute,
|
|
6
|
+
* controlled by `priority` / `lazy`.
|
|
7
|
+
* 2. Blur-up placeholder `placeholder="blur"` renders a blurred
|
|
8
|
+
* background image (from `blurDataURL`) or a
|
|
9
|
+
* solid color until the real image loads, then
|
|
10
|
+
* smoothly transitions to sharp.
|
|
11
|
+
* 3. Loading / error state `onLoad` / `onError` callbacks fire once per
|
|
12
|
+
* transition; the element also exposes the
|
|
13
|
+
* state via `data-pyxle-image-state`.
|
|
14
|
+
* 4. Graceful fallback An optional `fallbackSrc` kicks in on error
|
|
15
|
+
* so a broken image URL doesn't leave a blank
|
|
16
|
+
* box (can still be combined with `onError`).
|
|
17
|
+
*
|
|
18
|
+
* Everything else — `srcSet`, `sizes`, `className`, `style`, `onClick`, … —
|
|
19
|
+
* passes straight through to the underlying <img>.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useEffect, useRef, useState } from 'react';
|
|
23
|
+
|
|
24
|
+
const STATE_LOADING = 'loading';
|
|
25
|
+
const STATE_LOADED = 'loaded';
|
|
26
|
+
const STATE_ERROR = 'error';
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export function Image({
|
|
30
|
+
src,
|
|
31
|
+
alt = '',
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
priority = false,
|
|
35
|
+
lazy = true,
|
|
36
|
+
placeholder = 'empty',
|
|
37
|
+
blurDataURL,
|
|
38
|
+
placeholderColor = '#e5e5e5',
|
|
39
|
+
fallbackSrc,
|
|
40
|
+
onLoad,
|
|
41
|
+
onError,
|
|
42
|
+
className,
|
|
43
|
+
style,
|
|
44
|
+
...props
|
|
45
|
+
}) {
|
|
46
|
+
const [state, setState] = useState(STATE_LOADING);
|
|
47
|
+
const [currentSrc, setCurrentSrc] = useState(src);
|
|
48
|
+
const imgRef = useRef(null);
|
|
49
|
+
|
|
50
|
+
// Reset loading state when src changes.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setState(STATE_LOADING);
|
|
53
|
+
setCurrentSrc(src);
|
|
54
|
+
}, [src]);
|
|
55
|
+
|
|
56
|
+
// If an image is already cached by the browser (common on hover-prefetch
|
|
57
|
+
// or client-side navigation), the `load` event never fires on the new
|
|
58
|
+
// element. Check `complete` after mount and sync state manually.
|
|
59
|
+
//
|
|
60
|
+
// Symmetrically: when SSR renders an `<img>` with a broken `src`, the
|
|
61
|
+
// browser may finish the failed fetch before React hydrates, meaning the
|
|
62
|
+
// synthetic `onError` listener is attached too late to see the event. In
|
|
63
|
+
// that case `complete` is still true but `naturalWidth` is 0 — treat as
|
|
64
|
+
// error and drive the fallback / onError path.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const el = imgRef.current;
|
|
67
|
+
if (!el || !el.complete || state !== STATE_LOADING) return;
|
|
68
|
+
|
|
69
|
+
if (el.naturalWidth > 0) {
|
|
70
|
+
setState(STATE_LOADED);
|
|
71
|
+
if (onLoad) onLoad({ nativeEvent: null, target: el, fromCache: true });
|
|
72
|
+
} else {
|
|
73
|
+
// Image finished fetching but has no pixels — treat as error.
|
|
74
|
+
if (fallbackSrc && currentSrc !== fallbackSrc) {
|
|
75
|
+
setCurrentSrc(fallbackSrc);
|
|
76
|
+
} else {
|
|
77
|
+
setState(STATE_ERROR);
|
|
78
|
+
if (onError) onError({ nativeEvent: null, target: el });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Only run on mount and when src changes; callbacks intentionally omitted.
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
}, [currentSrc]);
|
|
84
|
+
|
|
85
|
+
function handleLoad(event) {
|
|
86
|
+
setState(STATE_LOADED);
|
|
87
|
+
if (onLoad) onLoad(event);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleError(event) {
|
|
91
|
+
if (fallbackSrc && currentSrc !== fallbackSrc) {
|
|
92
|
+
setCurrentSrc(fallbackSrc);
|
|
93
|
+
return; // wait for the fallback to load / fail
|
|
94
|
+
}
|
|
95
|
+
setState(STATE_ERROR);
|
|
96
|
+
if (onError) onError(event);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const showPlaceholder = placeholder === 'blur' && state === STATE_LOADING;
|
|
100
|
+
|
|
101
|
+
const mergedStyle = {
|
|
102
|
+
...(showPlaceholder
|
|
103
|
+
? {
|
|
104
|
+
backgroundColor: blurDataURL ? undefined : placeholderColor,
|
|
105
|
+
backgroundImage: blurDataURL ? `url("${blurDataURL}")` : undefined,
|
|
106
|
+
backgroundSize: 'cover',
|
|
107
|
+
backgroundPosition: 'center',
|
|
108
|
+
backgroundRepeat: 'no-repeat',
|
|
109
|
+
filter: blurDataURL ? 'blur(20px)' : undefined,
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
// Smooth transition once the image renders on top.
|
|
113
|
+
transition: placeholder === 'blur' ? 'filter 250ms ease-out' : undefined,
|
|
114
|
+
...style,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<img
|
|
119
|
+
ref={imgRef}
|
|
120
|
+
src={currentSrc}
|
|
121
|
+
alt={alt}
|
|
122
|
+
width={width}
|
|
123
|
+
height={height}
|
|
124
|
+
loading={priority ? 'eager' : lazy ? 'lazy' : 'eager'}
|
|
125
|
+
decoding={priority ? 'sync' : 'async'}
|
|
126
|
+
onLoad={handleLoad}
|
|
127
|
+
onError={handleError}
|
|
128
|
+
className={className}
|
|
129
|
+
style={mergedStyle}
|
|
130
|
+
data-pyxle-image-state={state}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|