diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index dcdf54342..0a93205e0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,8 +8,6 @@ Always reference these instructions first and fallback to search or bash command **BUG INVESTIGATION**: When investigating whether a bug was already resolved in a previous version, always prioritize searching through `docs/source/about/changelog.rst` first before using Git history. Only search through Git history when no relevant changelog entries are found. -**CODE RETRIEVAL**: Always prioritize using Serena tools (e.g., `mcp_oraios_serena_find_symbol`, `mcp_oraios_serena_search_for_pattern`) for code retrieval and analysis over standard file reading or searching tools. - ## Working Effectively ### Bootstrap, Build, and Test the Repository @@ -56,7 +54,6 @@ pip install flask sanic tornado - `hatch test --cover` -- run tests with coverage reporting (used in CI) - `hatch test -k test_name` -- run specific tests - `hatch test tests/test_config.py` -- run specific test files -- Note: Some tests require Playwright browser automation and may fail in headless environments **Run Python Linting and Formatting:** @@ -70,7 +67,7 @@ pip install flask sanic tornado - `hatch run javascript:check` -- Lint and type-check JavaScript (10 seconds). NEVER CANCEL. Set timeout to 30+ minutes. - `hatch run javascript:fix` -- Format JavaScript code -- `hatch run javascript:test` -- Run JavaScript tests (note: may fail in headless environments due to DOM dependencies) +- `hatch run javascript:test` -- Run JavaScript tests **Interactive Development Shell:** @@ -325,13 +322,6 @@ Follow this step-by-step process for effective development: - Network timeouts during pip install are common in CI environments - Missing dependencies error: Install ASGI dependencies with `pip install orjson asgiref asgi-tools servestatic` -### Test Issues: - -- Playwright tests may fail in headless environments -- this is expected -- Tests requiring browser DOM should be marked appropriately -- Use `hatch test -k "not playwright"` to skip browser-dependent tests -- JavaScript tests may fail with "window is not defined" in Node.js environment -- this is expected - ### Import Issues: - ReactPy must be installed or src/ must be in Python path @@ -397,7 +387,6 @@ Always ensure your changes pass local validation before pushing, as the CI pipel - **All builds and tests run quickly** - if something takes more than 60 seconds, investigate the issue - **Hatch environments provide full isolation** - no need to manage virtual environments manually - **JavaScript packages are bundled into Python** - the build process combines JS and Python into a single distribution -- **Browser automation tests may fail in headless environments** - this is expected behavior for Playwright tests - **Documentation updates are required** when making changes to Python source code - **Always update this file** when making changes to the development workflow, build process, or repository structure - **All tests must always pass** - failures are never expected or allowed in a healthy development environment diff --git a/.gitignore b/.gitignore index 40cd524ba..535e615ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # --- Build Artifacts --- -src/reactpy/static/index.js* +src/reactpy/static/*.js* src/reactpy/static/morphdom/ src/reactpy/static/pyscript/ diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 14d86ad62..000000000 --- a/.serena/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cache diff --git a/.serena/memories/code_style.md b/.serena/memories/code_style.md deleted file mode 100644 index cd9ad1ecd..000000000 --- a/.serena/memories/code_style.md +++ /dev/null @@ -1,8 +0,0 @@ -# Code Style & Conventions - -- **Formatting**: Enforced by `hatch fmt`. -- **Type Hints**: Required, checked by `hatch run python:type_check`. -- **JS Linting**: Enforced by `hatch run javascript:check`. -- **Tests**: All tests must pass. Failures not allowed. -- **Documentation**: Must be updated with code changes. -- **Changelog**: Required for significant changes. diff --git a/.serena/memories/development_workflow.md b/.serena/memories/development_workflow.md deleted file mode 100644 index e2d0cc191..000000000 --- a/.serena/memories/development_workflow.md +++ /dev/null @@ -1,12 +0,0 @@ -# Development Workflow - -1. **Bootstrap**: Install Python 3.9+, Hatch, Bun. -2. **Changes**: Modify code. -3. **Format**: `hatch fmt`. -4. **Type Check**: `hatch run python:type_check`. -5. **JS Check**: `hatch run javascript:check` (if JS changed). -6. **Test**: `hatch test`. -7. **Validate**: Manual component/server tests. -8. **Build JS**: `hatch run javascript:build` (if JS changed). -9. **Docs**: Update if Python source changed. -10. **Changelog**: Add entry in `docs/source/about/changelog.rst`. diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md deleted file mode 100644 index b36012911..000000000 --- a/.serena/memories/project_overview.md +++ /dev/null @@ -1,16 +0,0 @@ -# ReactPy Overview - -## Purpose -ReactPy builds UIs in Python without JS, using React-like components and a Python-to-JS bridge. - -## Tech Stack -- **Python**: 3.9+ -- **Build**: Hatch -- **JS Runtime**: Bun -- **Deps**: fastjsonschema, requests, lxml, anyio - -## Structure -- `src/reactpy/`: Python source (core, web, executors). -- `src/js/`: JS packages (client, app). -- `tests/`: Test suite. -- `docs/`: Documentation. diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md deleted file mode 100644 index 66f252da4..000000000 --- a/.serena/memories/suggested_commands.md +++ /dev/null @@ -1,17 +0,0 @@ -# Suggested Commands (Hatch) - -## Python -- **Test**: `hatch test` (All must pass) -- **Coverage**: `hatch test --cover` -- **Format**: `hatch fmt` -- **Check Format**: `hatch fmt --check` -- **Type Check**: `hatch run python:type_check` -- **Build**: `hatch build --clean` - -## JavaScript -- **Build**: `hatch run javascript:build` (Rebuild after JS changes) -- **Check**: `hatch run javascript:check` -- **Fix**: `hatch run javascript:fix` - -## Shell -- **Dev Shell**: `hatch shell` diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index fa4652a00..000000000 --- a/.serena/project.yml +++ /dev/null @@ -1,84 +0,0 @@ -# list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp csharp_omnisharp -# dart elixir elm erlang fortran go -# haskell java julia kotlin lua markdown -# nix perl php python python_jedi r -# rego ruby ruby_solargraph rust scala swift -# terraform typescript typescript_vts yaml zig -# Note: -# - For C, use cpp -# - For JavaScript, use typescript -# Special requirements: -# - csharp: Requires the presence of a .sln file in the project folder. -# When using multiple languages, the first language server that supports a given file will be used for that file. -# The first language is the default language and the respective language server will be used as a fallback. -# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. -languages: -- python - -# the encoding used by text files in the project -# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings -encoding: "utf-8" - -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 -ignore_all_files_in_gitignore: true - -# list of additional paths to ignore -# same syntax as gitignore, so you can use * and ** -# Was previously called `ignored_dirs`, please update your config if you are using that. -# Added (renamed) on 2025-04-07 -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -project_name: "reactpy" -included_optional_tools: [] diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index 8f7e87c78..6ea0fd6b0 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -33,12 +33,15 @@ Unreleased - :pull:`1269` - Added ``reactpy.pyscript_component`` that can be used to embed ReactPy components into your existing application. - :pull:`1264` - Added ``reactpy.use_async_effect`` hook. - :pull:`1281` - Added ``reactpy.Vdom`` primitive interface for creating VDOM dictionaries. -- :pull:`1307` - Added ``reactpy.web.reactjs_component_from_file`` to import ReactJS components from a file. -- :pull:`1307` - Added ``reactpy.web.reactjs_component_from_url`` to import ReactJS components from a URL. -- :pull:`1307` - Added ``reactpy.web.reactjs_component_from_string`` to import ReactJS components from a string. +- :pull:`1307` - Added ``reactpy.reactjs.component_from_file`` to import ReactJS components from a file. +- :pull:`1307` - Added ``reactpy.reactjs.component_from_url`` to import ReactJS components from a URL. +- :pull:`1307` - Added ``reactpy.reactjs.component_from_string`` to import ReactJS components from a string. +- :pull:`1314` - Added ``reactpy.reactjs.component_from_npm`` to import ReactJS components from NPM. +- :pull:`1314` - Added ``reactpy.h`` as a shorthand alias for ``reactpy.html``. **Changed** +- :pull:`1314` - The ``key`` attribute is now stored within ``attributes`` in the VDOM spec. - :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``. - :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML scripts. - :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ```` element by calling ``html.data_table()``. @@ -59,14 +62,15 @@ Unreleased - :pull:`1281` - ``reactpy.core.vdom._EllipsisRepr`` has been moved to ``reactpy.types.EllipsisRepr``. - :pull:`1281` - ``reactpy.types.VdomDictConstructor`` has been renamed to ``reactpy.types.VdomConstructor``. - :pull:`1312` - ``REACTPY_ASYNC_RENDERING`` can now de-duplicate and cascade renders where necessary. -- :pull:`1312` - ``REACTPY_ASYNC_RENDERING`` is now defaulted to ``True`` for up to 40x performance improvements. +- :pull:`1312` - ``REACTPY_ASYNC_RENDERING`` is now defaulted to ``True`` for up to 40x performance improvements in environments with high concurrency. **Deprecated** --:pull:`1307` - ``reactpy.web.export`` is deprecated. Use ``reactpy.web.reactjs_component_from_*`` instead. --:pull:`1307` - ``reactpy.web.module_from_file`` is deprecated. Use ``reactpy.web.reactjs_component_from_file`` instead. --:pull:`1307` - ``reactpy.web.module_from_url`` is deprecated. Use ``reactpy.web.reactjs_component_from_url`` instead. --:pull:`1307` - ``reactpy.web.module_from_string`` is deprecated. Use ``reactpy.web.reactjs_component_from_string`` instead. +-:pull:`1307` - ``reactpy.web.export`` is deprecated. Use ``reactpy.reactjs.component_from_*`` instead. +-:pull:`1307` - ``reactpy.web.module_from_file`` is deprecated. Use ``reactpy.reactjs.component_from_file`` instead. +-:pull:`1307` - ``reactpy.web.module_from_url`` is deprecated. Use ``reactpy.reactjs.component_from_url`` instead. +-:pull:`1307` - ``reactpy.web.module_from_string`` is deprecated. Use ``reactpy.reactjs.component_from_string`` instead. +-:pull:`1314` - ``reactpy.web.*`` is deprecated. Use ``reactpy.reactjs.*`` instead. **Removed** @@ -78,8 +82,8 @@ Unreleased - :pull:`1311` - Removed deprecated exception type ``reactpy.core.serve.Stop``. - :pull:`1311` - Removed deprecated component ``reactpy.widgets.hotswap``. - :pull:`1255` - Removed ``reactpy.sample`` module. -- :pull:`1255` - Removed ``reactpy.svg`` module. Contents previously within ``reactpy.svg.*`` can now be accessed via ``html.svg.*``. -- :pull:`1255` - Removed ``reactpy.html._`` function. Use ``html.fragment`` instead. +- :pull:`1255` - Removed ``reactpy.svg`` module. Contents previously within ``reactpy.svg.*`` can now be accessed via ``reactpy.html.svg.*``. +- :pull:`1255` - Removed ``reactpy.html._`` function. Use ``reactpy.html(...)`` or ``reactpy.html.fragment(...)`` instead. - :pull:`1113` - Removed ``reactpy.run``. See the documentation for the new method to run ReactPy applications. - :pull:`1113` - Removed ``reactpy.backend.*``. See the documentation for the new method to run ReactPy applications. - :pull:`1113` - Removed ``reactpy.core.types`` module. Use ``reactpy.types`` instead. @@ -92,12 +96,14 @@ Unreleased - :pull:`1312` - Removed ``reactpy.types.LayoutType``. Use ``reactpy.types.BaseLayout`` instead. - :pull:`1312` - Removed ``reactpy.types.ContextProviderType``. Use ``reactpy.types.ContextProvider`` instead. - :pull:`1312` - Removed ``reactpy.core.hooks._ContextProvider``. Use ``reactpy.types.ContextProvider`` instead. +- :pull:`1314` - Removed ``reactpy.web.utils``. Use ``reactpy.reactjs.utils`` instead. **Fixed** - :pull:`1239` - Fixed a bug where script elements would not render to the DOM as plain text. - :pull:`1271` - Fixed a bug where the ``key`` property provided within server-side ReactPy code was failing to propagate to the front-end JavaScript components. - :pull:`1254` - Fixed a bug where ``RuntimeError("Hook stack is in an invalid state")`` errors could be generated when using a webserver that reuses threads. +- :pull:`1314` - Allow for ReactPy and ReactJS components to be arbitrarily inserted onto the page with any possible hierarchy. v1.1.0 diff --git a/src/build_scripts/clean_js_dir.py b/src/build_scripts/clean_js_dir.py index 05db847e6..cdb9e276a 100644 --- a/src/build_scripts/clean_js_dir.py +++ b/src/build_scripts/clean_js_dir.py @@ -13,29 +13,30 @@ # Get the path to the JS source directory js_src_dir = pathlib.Path(__file__).parent.parent / "js" - -# Get the paths to all `dist` folders in the JS source directory -dist_dirs = glob.glob(str(js_src_dir / "**/dist"), recursive=True) - -# Get the paths to all `node_modules` folders in the JS source directory -node_modules_dirs = glob.glob(str(js_src_dir / "**/node_modules"), recursive=True) - -# Get the paths to all `tsconfig.tsbuildinfo` files in the JS source directory -tsconfig_tsbuildinfo_files = glob.glob( - str(js_src_dir / "**/tsconfig.tsbuildinfo"), recursive=True -) +static_output_dir = pathlib.Path(__file__).parent.parent / "reactpy" / "static" # Delete all `dist` folders +dist_dirs = glob.glob(str(js_src_dir / "**/dist"), recursive=True) for dist_dir in dist_dirs: with contextlib.suppress(FileNotFoundError): shutil.rmtree(dist_dir) # Delete all `node_modules` folders +node_modules_dirs = glob.glob(str(js_src_dir / "**/node_modules"), recursive=True) for node_modules_dir in node_modules_dirs: with contextlib.suppress(FileNotFoundError): shutil.rmtree(node_modules_dir) # Delete all `tsconfig.tsbuildinfo` files +tsconfig_tsbuildinfo_files = glob.glob( + str(js_src_dir / "**/tsconfig.tsbuildinfo"), recursive=True +) for tsconfig_tsbuildinfo_file in tsconfig_tsbuildinfo_files: with contextlib.suppress(FileNotFoundError): os.remove(tsconfig_tsbuildinfo_file) + +# Delete all `index-*.js` files +index_js_files = glob.glob(str(static_output_dir / "index-*.js*")) +for index_js_file in index_js_files: + with contextlib.suppress(FileNotFoundError): + os.remove(index_js_file) diff --git a/src/js/packages/@reactpy/app/package.json b/src/js/packages/@reactpy/app/package.json index c23e50f65..488a1742c 100644 --- a/src/js/packages/@reactpy/app/package.json +++ b/src/js/packages/@reactpy/app/package.json @@ -13,7 +13,7 @@ "license": "MIT", "name": "@reactpy/app", "scripts": { - "build": "bun build \"src/index.ts\" --outdir=\"../../../../reactpy/static/\" --minify --sourcemap=\"linked\"", + "build": "bun build \"src/index.ts\" \"src/react.ts\" \"src/react-dom.ts\" \"src/react-jsx-runtime.ts\" --outdir=\"../../../../reactpy/static/\" --minify --sourcemap=\"linked\" --splitting", "checkTypes": "tsc --noEmit" } } diff --git a/src/js/packages/@reactpy/app/src/react-dom.ts b/src/js/packages/@reactpy/app/src/react-dom.ts new file mode 100644 index 000000000..17d1e16f1 --- /dev/null +++ b/src/js/packages/@reactpy/app/src/react-dom.ts @@ -0,0 +1,9 @@ +import ReactDOM from "preact/compat"; + +// @ts-ignore +export * from "preact/compat"; + +// @ts-ignore +export * from "preact/compat/client"; + +export default ReactDOM; diff --git a/src/js/packages/@reactpy/app/src/react-jsx-runtime.ts b/src/js/packages/@reactpy/app/src/react-jsx-runtime.ts new file mode 100644 index 000000000..9e7e8fc51 --- /dev/null +++ b/src/js/packages/@reactpy/app/src/react-jsx-runtime.ts @@ -0,0 +1 @@ +export * from "preact/jsx-runtime"; diff --git a/src/js/packages/@reactpy/app/src/react.ts b/src/js/packages/@reactpy/app/src/react.ts new file mode 100644 index 000000000..21f104942 --- /dev/null +++ b/src/js/packages/@reactpy/app/src/react.ts @@ -0,0 +1,6 @@ +import React from "preact/compat"; + +// @ts-ignore +export * from "preact/compat"; + +export default React; diff --git a/src/js/packages/@reactpy/app/tsconfig.json b/src/js/packages/@reactpy/app/tsconfig.json index e7c43004b..de3ec6378 100644 --- a/src/js/packages/@reactpy/app/tsconfig.json +++ b/src/js/packages/@reactpy/app/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "composite": true, "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "esModuleInterop": true }, "extends": "../../../tsconfig.json", "include": ["src"], diff --git a/src/js/packages/@reactpy/client/package.json b/src/js/packages/@reactpy/client/package.json index d8e2fcefd..bef8fa4b5 100644 --- a/src/js/packages/@reactpy/client/package.json +++ b/src/js/packages/@reactpy/client/package.json @@ -31,5 +31,5 @@ "checkTypes": "tsc --noEmit" }, "type": "module", - "version": "1.0.2" + "version": "1.0.3" } diff --git a/src/js/packages/@reactpy/client/src/components.tsx b/src/js/packages/@reactpy/client/src/components.tsx index a4fa97ce3..d7c5825bd 100644 --- a/src/js/packages/@reactpy/client/src/components.tsx +++ b/src/js/packages/@reactpy/client/src/components.tsx @@ -67,7 +67,7 @@ function StandardElement({ model }: { model: ReactPyVdom }) { model.tagName === "" ? Fragment : model.tagName, createAttributes(model, client), ...createChildren(model, (child) => { - return ; + return ; }), ); } @@ -100,7 +100,7 @@ function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element { // overwrite { ...props, value }, ...createChildren(model, (child) => ( - + )), ); } @@ -135,7 +135,7 @@ function ScriptElement({ model }: { model: ReactPyVdom }) { return () => { ref.current?.removeChild(scriptElement); }; - }, [model.key]); + }, [model.attributes?.key]); return
; } diff --git a/src/js/packages/@reactpy/client/src/index.ts b/src/js/packages/@reactpy/client/src/index.ts index b173a4226..cca51fc00 100644 --- a/src/js/packages/@reactpy/client/src/index.ts +++ b/src/js/packages/@reactpy/client/src/index.ts @@ -6,4 +6,5 @@ export * from "./vdom"; export * from "./websocket"; export { default as React } from "preact/compat"; export { default as ReactDOM } from "preact/compat"; +export { jsx, jsxs, Fragment } from "preact/jsx-runtime"; export * as preact from "preact"; diff --git a/src/js/packages/@reactpy/client/src/types.ts b/src/js/packages/@reactpy/client/src/types.ts index 49232e532..12bc8f3fa 100644 --- a/src/js/packages/@reactpy/client/src/types.ts +++ b/src/js/packages/@reactpy/client/src/types.ts @@ -48,7 +48,6 @@ export type ReactPyComponent = ComponentType<{ model: ReactPyVdom }>; export type ReactPyVdom = { tagName: string; - key?: string; attributes?: { [key: string]: string }; children?: (ReactPyVdom | string)[]; error?: string; diff --git a/src/js/packages/@reactpy/client/src/vdom.tsx b/src/js/packages/@reactpy/client/src/vdom.tsx index b94ba3003..54999bf1f 100644 --- a/src/js/packages/@reactpy/client/src/vdom.tsx +++ b/src/js/packages/@reactpy/client/src/vdom.tsx @@ -8,6 +8,7 @@ import type { ReactPyModule, BindImportSource, ReactPyModuleBinding, + ImportSourceBinding, } from "./types"; import log from "./logger"; @@ -75,13 +76,15 @@ function createImportSourceElement(props: { if ( !isImportSourceEqual(props.currentImportSource, props.model.importSource) ) { - log.error( - "Parent element import source " + - stringifyImportSource(props.currentImportSource) + - " does not match child's import source " + - stringifyImportSource(props.model.importSource), - ); - return null; + return props.binding.create("reactpy-child", { + ref: (node: ReactPyChild | null) => { + if (node) { + node.client = props.client; + node.model = props.model; + node.requestUpdate(); + } + }, + }); } else { type = getComponentFromModule( props.module, @@ -270,3 +273,85 @@ function generic_reactjs_bind(node: HTMLElement) { unmount: () => preact.render(null, node), }; } + +class ReactPyChild extends HTMLElement { + mountPoint: HTMLDivElement; + binding: ImportSourceBinding | null = null; + _client: ReactPyClientInterface | null = null; + _model: ReactPyVdom | null = null; + currentImportSource: ReactPyVdomImportSource | null = null; + + constructor() { + super(); + this.mountPoint = document.createElement("div"); + this.mountPoint.style.display = "contents"; + } + + connectedCallback() { + this.appendChild(this.mountPoint); + } + + set client(value: ReactPyClientInterface) { + this._client = value; + } + + set model(value: ReactPyVdom) { + this._model = value; + } + + requestUpdate() { + this.update(); + } + + async update() { + if (!this._client || !this._model || !this._model.importSource) { + return; + } + + const newImportSource = this._model.importSource; + + if ( + !this.binding || + !this.currentImportSource || + !isImportSourceEqual(this.currentImportSource, newImportSource) + ) { + if (this.binding) { + this.binding.unmount(); + this.binding = null; + } + + this.currentImportSource = newImportSource; + + try { + const bind = await loadImportSource(newImportSource, this._client); + if (this.isConnected) { + this.binding = bind(this.mountPoint); + if (this.binding) { + this.binding.render(this._model); + } + } + } catch (error) { + console.error("Failed to load import source", error); + } + } else { + if (this.binding) { + this.binding.render(this._model); + } + } + } + + disconnectedCallback() { + if (this.binding) { + this.binding.unmount(); + this.binding = null; + this.currentImportSource = null; + } + } +} + +if ( + typeof customElements !== "undefined" && + !customElements.get("reactpy-child") +) { + customElements.define("reactpy-child", ReactPyChild); +} diff --git a/src/reactpy/__init__.py b/src/reactpy/__init__.py index fc6c5332e..3166add07 100644 --- a/src/reactpy/__init__.py +++ b/src/reactpy/__init__.py @@ -1,4 +1,4 @@ -from reactpy import config, logging, types, web, widgets +from reactpy import config, logging, reactjs, types, web, widgets from reactpy._html import html from reactpy.core import hooks from reactpy.core.component import component @@ -36,6 +36,7 @@ "html", "logging", "pyscript_component", + "reactjs", "reactpy_to_string", "string_to_reactpy", "types", diff --git a/src/reactpy/_html.py b/src/reactpy/_html.py index ffeee7072..9cdc367d3 100644 --- a/src/reactpy/_html.py +++ b/src/reactpy/_html.py @@ -6,7 +6,6 @@ from reactpy.core.vdom import Vdom from reactpy.types import ( EventHandlerDict, - Key, VdomAttributes, VdomChild, VdomChildren, @@ -14,7 +13,7 @@ VdomDict, ) -__all__ = ["html"] +__all__ = ["h", "html"] NO_CHILDREN_ALLOWED_HTML_BODY = { "area", @@ -100,12 +99,10 @@ def _fragment( attributes: VdomAttributes, children: Sequence[VdomChild], - key: Key | None, event_handlers: EventHandlerDict, ) -> VdomDict: """An HTML fragment - this element will not appear in the DOM""" - attributes.pop("key", None) - if attributes or event_handlers: + if any(k != "key" for k in attributes) or event_handlers: msg = "Fragments cannot have attributes besides 'key'" raise TypeError(msg) model = VdomDict(tagName="") @@ -113,8 +110,8 @@ def _fragment( if children: model["children"] = children - if key is not None: - model["key"] = key + if attributes: + model["attributes"] = attributes return model @@ -122,7 +119,6 @@ def _fragment( def _script( attributes: VdomAttributes, children: Sequence[VdomChild], - key: Key | None, event_handlers: EventHandlerDict, ) -> VdomDict: """Create a new `