You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by pi...@apache.org on 2025/07/21 10:23:47 UTC

(airflow) branch main updated: AIP-68 CLI to bootstrap a React App plugin (#53365)

This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 0f0ea3b5c44 AIP-68 CLI to bootstrap a React App plugin (#53365)
0f0ea3b5c44 is described below

commit 0f0ea3b5c444465777a7c5eb5188b301faed1aee
Author: Pierre Jeambrun <pi...@gmail.com>
AuthorDate: Mon Jul 21 12:23:41 2025 +0200

    AIP-68 CLI to bootstrap a React App plugin (#53365)
    
    * First working iteration
    
    * Remove apache licence when generating the project
    
    * Incorporate the theme, and small adjustments
    
    * Fix linting errors
    
    * Integrate to the airflow CLI
    
    * Update test structure
    
    * Add unit tests
    
    * Small adjustments
    
    * Address PR comment, build as library
    
    * Fix react issues
    
    * Smaller dependencies
    
    * Move back dump command to CLI plugins
    
    * Move back command to dev folder
    
    * Update airflow-core/src/airflow/cli/cli_config.py
    
    Co-authored-by: Jens Scheffler <95...@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Jens Scheffler <95...@users.noreply.github.com>
---
 airflow-core/src/airflow/ui/src/main.tsx           |   8 +
 .../src/airflow/ui/src/pages/ReactPlugin.tsx       |  31 +++-
 airflow-core/src/airflow/ui/tsconfig.node.json     |   1 +
 dev/react-plugin-tools/README.md                   |  59 +++++++
 dev/react-plugin-tools/bootstrap.py                | 179 +++++++++++++++++++++
 .../react_plugin_template/.gitignore               |  28 ++++
 .../react_plugin_template/.prettierignore          |   5 +
 .../react_plugin_template/.prettierrc              |  13 ++
 .../react_plugin_template/README.md                | 138 ++++++++++++++++
 .../react_plugin_template/eslint.config.js         |  58 +++++++
 .../react_plugin_template/index.html               |  13 ++
 .../react_plugin_template/package.json             |  75 +++++++++
 .../src/context/colorMode/ColorModeProvider.tsx    |  25 +++
 .../src/context/colorMode/index.ts                 |  21 +++
 .../src/context/colorMode/useColorMode.tsx         |  33 ++++
 .../react_plugin_template/src/dev.tsx              |  30 ++++
 .../react_plugin_template/src/main.tsx             |  39 +++--
 .../react_plugin_template/src/pages/HomePage.tsx   |  38 ++---
 .../react_plugin_template/src/theme.ts             | 176 ++++++++++++++++++++
 .../react_plugin_template/src/vite-env.d.ts        |  20 +++
 .../react_plugin_template/testsSetup.ts            |  20 +++
 .../react_plugin_template/tsconfig.app.json        |  10 +-
 .../react_plugin_template/tsconfig.json            |   8 +
 .../react_plugin_template/tsconfig.lib.json        |  15 ++
 .../react_plugin_template}/tsconfig.node.json      |   4 +-
 .../react_plugin_template/vite.config.ts           |  84 ++++++++++
 26 files changed, 1077 insertions(+), 54 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx
index 2945773d181..2768d84809a 100644
--- a/airflow-core/src/airflow/ui/src/main.tsx
+++ b/airflow-core/src/airflow/ui/src/main.tsx
@@ -20,9 +20,11 @@ import { ChakraProvider } from "@chakra-ui/react";
 import { QueryClientProvider } from "@tanstack/react-query";
 import axios, { type AxiosError } from "axios";
 import { StrictMode } from "react";
+import React from "react";
 import { createRoot } from "react-dom/client";
 import { I18nextProvider } from "react-i18next";
 import { RouterProvider } from "react-router-dom";
+import * as ReactJSXRuntime from "react/jsx-runtime";
 
 import type { HTTPExceptionResponse } from "openapi/requests/types.gen";
 import { ColorModeProvider } from "src/context/colorMode";
@@ -35,6 +37,12 @@ import { client } from "./queryClient";
 import { system } from "./theme";
 import { clearToken, tokenHandler } from "./utils/tokenHandler";
 
+// Set React and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins.
+// Only one instance of React should be used.
+// Reflect will avoid type checking.
+Reflect.set(globalThis, "React", React);
+Reflect.set(globalThis, "ReactJSXRuntime", ReactJSXRuntime);
+
 // redirect to login page if the API responds with unauthorized or forbidden errors
 axios.interceptors.response.use(
   (response) => response,
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
index 0f80af777c8..8a193205634 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { Spinner } from "@chakra-ui/react";
-import { lazy, Suspense } from "react";
+import { type FC, lazy, Suspense } from "react";
 import { useParams } from "react-router-dom";
 
 import type { ReactAppResponse } from "openapi/requests/types.gen";
@@ -29,13 +29,30 @@ export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse
 
   const Plugin = lazy(() =>
     // We are assuming the plugin manager is trusted and the bundle_url is safe
-    import(/* @vite-ignore */ reactApp.bundle_url).catch((error: unknown) => {
-      console.error("Component Failed Loading:", error);
+    import(/* @vite-ignore */ reactApp.bundle_url)
+      .then(() => {
+        const component = (
+          globalThis as unknown as {
+            AirflowPlugin: FC<{
+              dagId?: string;
+              mapIndex?: string;
+              runId?: string;
+              taskId?: string;
+            }>;
+          }
+        ).AirflowPlugin;
 
-      return {
-        default: <ErrorPage />,
-      };
-    }),
+        return {
+          default: component,
+        };
+      })
+      .catch((error: unknown) => {
+        console.error("Component Failed Loading:", error);
+
+        return {
+          default: ErrorPage,
+        };
+      }),
   );
 
   return (
diff --git a/airflow-core/src/airflow/ui/tsconfig.node.json b/airflow-core/src/airflow/ui/tsconfig.node.json
index a731173fc6c..1072cb12857 100644
--- a/airflow-core/src/airflow/ui/tsconfig.node.json
+++ b/airflow-core/src/airflow/ui/tsconfig.node.json
@@ -18,6 +18,7 @@
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noFallthroughCasesInSwitch": true,
+    "noUncheckedIndexedAccess": true,
     "baseUrl": ".",
     "paths": {
       "src/*": ["./src/*"],
diff --git a/dev/react-plugin-tools/README.md b/dev/react-plugin-tools/README.md
new file mode 100644
index 00000000000..9dc95e7dacb
--- /dev/null
+++ b/dev/react-plugin-tools/README.md
@@ -0,0 +1,59 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [React Plugin Development Tools](#react-plugin-development-tools)
+  - [Overview](#overview)
+  - [Files](#files)
+  - [Quick Start](#quick-start)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# React Plugin Development Tools
+
+This directory contains tools for developing React-based Airflow plugins that can be dynamically loaded into the Airflow UI.
+
+## Overview
+
+These tools help you create React plugin projects that:
+
+- Build as libraries compatible with dynamic imports
+- Share React instances with the host Airflow application
+- Follow Airflow's UI development patterns and standards
+- Include proper TypeScript configuration and build setup
+
+## Files
+
+- `bootstrap.py` - CLI tool to create new React plugin projects
+- `react_plugin_template/` - Template directory with all the necessary files
+
+## Quick Start
+
+### Create a New Plugin Project
+
+```bash
+# From the dev/react-plugin-tools directory
+python bootstrap.py my-awesome-plugin
+
+# Or specify a custom directory
+python bootstrap.py my-awesome-plugin --dir /path/to/my-projects/my-awesome-plugin
+```
diff --git a/dev/react-plugin-tools/bootstrap.py b/dev/react-plugin-tools/bootstrap.py
new file mode 100755
index 00000000000..3406b26fe92
--- /dev/null
+++ b/dev/react-plugin-tools/bootstrap.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Bootstrap React Plugin CLI Tool.
+
+This script provides a command-line interface to create new React UI plugin
+directories based on the airflow-core/ui project structure. It sets up all the
+necessary configuration files, dependencies, and basic structure for development
+with the same tooling as used in Airflow's core UI.
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import shutil
+import sys
+from pathlib import Path
+
+
+def get_template_dir() -> Path:
+    """Get the template directory path."""
+    script_dir = Path(__file__).parent
+    template_dir = script_dir / "react_plugin_template"
+
+    if not template_dir.exists():
+        print(f"Error: Template directory not found at {template_dir}")
+        sys.exit(1)
+
+    return template_dir
+
+
+def replace_template_variables(content: str, project_name: str) -> str:
+    """Replace template variables in file content."""
+    return content.replace("{{PROJECT_NAME}}", project_name)
+
+
+def remove_apache_license_header(content: str, file_extension: str) -> str:
+    """Remove Apache license header from file content based on file type."""
+    if file_extension in [".ts", ".tsx", ".js", ".jsx"]:
+        license_pattern = r"/\*!\s*\*\s*Licensed to the Apache Software Foundation.*?\*/\s*"
+        content = re.sub(license_pattern, "", content, flags=re.DOTALL)
+    elif file_extension in [".md"]:
+        license_pattern = r"<!--\s*Licensed to the Apache Software Foundation.*?-->\s*"
+        content = re.sub(license_pattern, "", content, flags=re.DOTALL)
+    elif file_extension in [".html"]:
+        license_pattern = r"<!--\s*Licensed to the Apache Software Foundation.*?-->\s*"
+        content = re.sub(license_pattern, "", content, flags=re.DOTALL)
+
+    return content
+
+
+def copy_template_files(template_dir: Path, project_path: Path, project_name: str) -> None:
+    for item in template_dir.rglob("*"):
+        if item.is_file():
+            # Calculate relative path from template root
+            rel_path = item.relative_to(template_dir)
+            target_path = project_path / rel_path
+
+            target_path.parent.mkdir(parents=True, exist_ok=True)
+
+            with open(item, encoding="utf-8") as f:
+                content = f.read()
+
+            content = replace_template_variables(content, project_name)
+
+            file_extension = item.suffix.lower()
+            content = remove_apache_license_header(content, file_extension)
+
+            with open(target_path, "w", encoding="utf-8") as f:
+                f.write(content)
+
+            print(f"  Created: {rel_path}")
+
+
+def bootstrap_react_plugin(args) -> None:
+    """Bootstrap a new React plugin project."""
+    project_name = args.name
+    target_dir = args.dir if args.dir else project_name
+
+    project_path = Path(target_dir).resolve()
+    template_dir = get_template_dir()
+
+    if project_path.exists():
+        print(f"Error: Directory '{project_path}' already exists!")
+        sys.exit(1)
+
+    if not project_name.replace("-", "").replace("_", "").isalnum():
+        print("Error: Project name should only contain letters, numbers, hyphens, and underscores")
+        sys.exit(1)
+
+    print(f"Creating React plugin project: {project_name}")
+    print(f"Target directory: {project_path}")
+    print(f"Template directory: {template_dir}")
+
+    project_path.mkdir(parents=True, exist_ok=True)
+
+    try:
+        # Copy template files
+        print("Copying template files...")
+        copy_template_files(template_dir, project_path, project_name)
+
+        print(f"\nāœ… Successfully created {project_name}!")
+        print("\nNext steps:")
+        print(f"  cd {target_dir}")
+        print("  pnpm install")
+        print("  pnpm dev")
+        print("\nHappy coding! šŸš€")
+
+    except Exception as e:
+        print(f"Error creating project: {e}")
+        if project_path.exists():
+            shutil.rmtree(project_path)
+        sys.exit(1)
+
+
+def main():
+    """Main CLI entry point."""
+    parser = argparse.ArgumentParser(
+        description="Bootstrap a new React UI plugin project",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+Examples:
+  python bootstrap.py my-plugin
+  python bootstrap.py my-plugin --dir /path/to/projects/my-plugin
+
+This will create a new React project with all the necessary configuration
+files, dependencies, and structure needed for Airflow plugin development.
+        """,
+    )
+
+    parser.add_argument(
+        "name",
+        help="Name of the React plugin project (letters, numbers, hyphens, and underscores only)",
+    )
+
+    parser.add_argument(
+        "--dir",
+        "-d",
+        help="Target directory for the project (defaults to project name)",
+    )
+
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Enable verbose output",
+    )
+
+    args = parser.parse_args()
+
+    try:
+        bootstrap_react_plugin(args)
+    except KeyboardInterrupt:
+        print("\n\nOperation cancelled by user.")
+        sys.exit(1)
+    except Exception as e:
+        print(f"Error: {e}")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/dev/react-plugin-tools/react_plugin_template/.gitignore b/dev/react-plugin-tools/react_plugin_template/.gitignore
new file mode 100644
index 00000000000..aa80e7d6f38
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.pnpm-store
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Coverage
+coverage
diff --git a/dev/react-plugin-tools/react_plugin_template/.prettierignore b/dev/react-plugin-tools/react_plugin_template/.prettierignore
new file mode 100644
index 00000000000..c745cd9169a
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/.prettierignore
@@ -0,0 +1,5 @@
+dist/
+*.md
+*.yaml
+coverage/*
+node_modules/
diff --git a/dev/react-plugin-tools/react_plugin_template/.prettierrc b/dev/react-plugin-tools/react_plugin_template/.prettierrc
new file mode 100644
index 00000000000..604ac7a3004
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/.prettierrc
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json.schemastore.org/prettierrc",
+  "endOfLine": "lf",
+  "importOrder": ["<THIRD_PARTY_MODULES>", "^src/", "^[./]"],
+  "importOrderSeparation": true,
+  "jsxSingleQuote": false,
+  "plugins": ["@trivago/prettier-plugin-sort-imports"],
+  "printWidth": 110,
+  "singleQuote": false,
+  "tabWidth": 2,
+  "trailingComma": "all",
+  "useTabs": false
+}
diff --git a/dev/react-plugin-tools/react_plugin_template/README.md b/dev/react-plugin-tools/react_plugin_template/README.md
new file mode 100644
index 00000000000..112ef528e5f
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/README.md
@@ -0,0 +1,138 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [{{PROJECT_NAME}}](#project_name)
+  - [Development](#development)
+  - [Best Practices](#best-practices)
+  - [Troubleshooting](#troubleshooting)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# {{PROJECT_NAME}}
+
+This is a React plugin template for Apache Airflow that builds as a library component.
+
+## Development
+
+This template is configured to build your React component as a library that can be consumed by other applications.
+
+### Available Scripts
+
+- `pnpm dev` - Start development server with hot reload
+- `pnpm build` - Build the library for production
+- `pnpm build:types` - Generate TypeScript declaration files only
+- `pnpm build:lib` - Build the JavaScript library only
+- `pnpm test` - Run tests
+- `pnpm lint` - Check code quality
+- `pnpm format` - Format code
+
+### Library Output
+
+When you run `pnpm build`, the template generates:
+
+- `dist/main.js` - ES module JavaScript library
+- `dist/main.d.ts` - TypeScript declaration file
+- Source maps for debugging
+
+### Usage as a Library
+
+After building, other applications can import your component:
+
+```typescript
+import { PluginComponent } from 'your-plugin-name';
+
+// Use in your React app
+<PluginComponent />
+```
+
+### Development Mode
+
+For development and testing, use `pnpm dev` which will:
+
+- Start a development server on port 5173
+- Load the component using `src/dev.tsx` entry point
+- Enable hot module replacement
+
+### Library Configuration
+
+The template is configured with:
+
+- **Vite** for fast building and development
+- **TypeScript** with declaration file generation
+- **CSS injection** - styles are automatically injected into the JavaScript bundle
+- **External dependencies** - React and other common libraries are marked as external to reduce bundle size
+
+### Customization
+
+1. **Component Props**: Update the `PluginComponentProps` interface in `src/main.tsx`
+2. **External Dependencies**: Modify the `external` array in `vite.config.ts`
+3. **Build Output**: Adjust library configuration in `vite.config.ts`
+
+### Package Configuration
+
+The `package.json` is configured with:
+
+- `main` and `module` pointing to the built library
+- `types` pointing to generated declaration files
+- `exports` for modern import/export support
+- `files` array specifying what gets published
+
+This ensures your plugin can be consumed as both a CommonJS and ES module with full TypeScript support.
+
+## Best Practices
+
+1. **Keep React External**: Always mark React ecosystem as external to avoid conflicts
+2. **Global Naming**: Use standardized global name (`AirflowPlugin`) for consistency
+3. **Error Handling**: Implement proper error boundaries and fallbacks
+4. **TypeScript**: Use proper typing for plugin props and exports
+5. **Bundle Size**: Monitor bundle size and externalize large dependencies if needed
+
+## Troubleshooting
+
+### Common Issues
+
+**"Failed to resolve module specifier 'react'"**
+
+- Ensure React is marked as external in `vite.config.ts`
+- Verify host application exposes React globally
+
+**"Cannot read properties of null (reading 'useState')"**
+
+- React instance mismatch - check external configuration
+- Verify global React is properly set in host application
+
+**"Objects are not valid as a React child"**
+
+- Ensure you're returning component functions, not JSX elements
+- Check that lazy loading returns proper component structure
+
+**MIME type issues**
+
+- Ensure `.js` and `.cjs` files are served with correct MIME type
+
+For more help, check the main project documentation.
+
+### Deployment to Airflow Plugins
+
+Once the development is complete, you can build the library using `pnpm build` and then host the content of the `dist` folder. You can do that on your own infrastructure or within airflow
+by adding static file serving to your api server via registering a plugin `fastapi_apps`. You can take a look at the [Airflow documentation](https://airflow.apache.org/docs/apache-airflow/stable/plugins.html) for more information on how to do that.
diff --git a/dev/react-plugin-tools/react_plugin_template/eslint.config.js b/dev/react-plugin-tools/react_plugin_template/eslint.config.js
new file mode 100644
index 00000000000..57b4176ed9f
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/eslint.config.js
@@ -0,0 +1,58 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import js from "@eslint/js";
+import globals from "globals";
+import perfectionist from "eslint-plugin-perfectionist";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import prettierConfig from "eslint-config-prettier";
+
+export default tseslint.config(
+  { ignores: ["dist"] },
+  {
+    extends: [js.configs.recommended, ...tseslint.configs.recommended],
+    files: ["**/*.{ts,tsx}"],
+    languageOptions: {
+      ecmaVersion: "latest",
+      globals: globals.browser,
+    },
+    plugins: {
+      perfectionist,
+      "react-hooks": reactHooks,
+      "react-refresh": reactRefresh,
+    },
+    rules: {
+      ...reactHooks.configs.recommended.rules,
+      "perfectionist/sort-objects": [
+        "error",
+        {
+          type: "natural",
+          order: "asc",
+        },
+      ],
+      "react-refresh/only-export-components": [
+        "warn",
+        { allowConstantExport: true },
+      ],
+    },
+  },
+  prettierConfig
+);
diff --git a/dev/react-plugin-tools/react_plugin_template/index.html b/dev/react-plugin-tools/react_plugin_template/index.html
new file mode 100644
index 00000000000..0c61743326d
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>{{PROJECT_NAME}} - Development</title>
+  </head>
+  <body>
+    <div id="root" style="width:100%;height:100%;position:fixed;"></div>
+    <script type="module" src="/src/dev.tsx"></script>
+  </body>
+</html>
diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json
new file mode 100644
index 00000000000..5f5ed1ffe82
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/package.json
@@ -0,0 +1,75 @@
+{
+  "name": "{{PROJECT_NAME}}",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "main": "./dist/main.js",
+  "module": "./dist/main.js",
+  "types": "./dist/main.d.ts",
+  "exports": {
+    ".": {
+      "import": "./dist/main.js",
+      "types": "./dist/main.d.ts"
+    }
+  },
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "dev": "vite --port 5173 --strictPort",
+    "build": "vite build",
+    "build:types": "tsc --p tsconfig.lib.json",
+    "build:lib": "vite build",
+    "lint": "eslint --quiet && tsc --p tsconfig.app.json",
+    "lint:fix": "eslint --fix && tsc --p tsconfig.app.json",
+    "format": "pnpm prettier --write .",
+    "preview": "vite preview",
+    "test": "vitest run",
+    "coverage": "vitest run --coverage"
+  },
+  "dependencies": {
+    "@chakra-ui/anatomy": "^2.3.4",
+    "@chakra-ui/react": "^3.20.0",
+    "@emotion/react": "^11.14.0",
+    "next-themes": "^0.3.0",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1"
+  },
+  "devDependencies": {
+    "@eslint/compat": "^1.2.9",
+    "@eslint/js": "^9.25.1",
+    "@stylistic/eslint-plugin": "^2.13.0",
+    "@testing-library/jest-dom": "^6.6.3",
+    "@testing-library/react": "^16.3.0",
+    "@trivago/prettier-plugin-sort-imports": "^4.3.0",
+    "@types/node": "^22.15.3",
+    "@types/react": "^18.3.19",
+    "@types/react-dom": "^18.3.5",
+    "@vitejs/plugin-react-swc": "^3.9.0",
+    "@vitest/coverage-v8": "^2.1.9",
+    "eslint": "^9.25.1",
+    "eslint-config-prettier": "^10.1.2",
+    "eslint-plugin-jsx-a11y": "^6.10.2",
+    "eslint-plugin-prettier": "^5.2.6",
+    "eslint-plugin-react": "^7.37.5",
+    "eslint-plugin-react-hooks": "^4.6.2",
+    "eslint-plugin-perfectionist": "^4.12.3",
+    "eslint-plugin-react-refresh": "^0.4.20",
+    "eslint-plugin-unicorn": "^55.0.0",
+    "globals": "^15.15.0",
+    "happy-dom": "^17.4.6",
+    "prettier": "^3.5.3",
+    "typescript": "~5.8.3",
+    "typescript-eslint": "^8.31.1",
+    "vite": "^5.4.19",
+    "vite-plugin-css-injected-by-js": "^3.5.2",
+    "vite-plugin-dts": "^4.3.0",
+    "vitest": "^2.1.9"
+  },
+  "pnpm": {
+    "onlyBuiltDependencies": [
+      "@swc/core",
+      "esbuild"
+    ]
+  }
+}
diff --git a/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/ColorModeProvider.tsx b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/ColorModeProvider.tsx
new file mode 100644
index 00000000000..2b0c93cd403
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/ColorModeProvider.tsx
@@ -0,0 +1,25 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ThemeProvider } from "next-themes";
+import type { ThemeProviderProps } from "next-themes/dist/types";
+
+export const ColorModeProvider = (props: ThemeProviderProps) => (
+  <ThemeProvider attribute="class" disableTransitionOnChange {...props} />
+);
diff --git a/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/index.ts b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/index.ts
new file mode 100644
index 00000000000..30edc0a3216
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/index.ts
@@ -0,0 +1,21 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from "./ColorModeProvider";
+export * from "./useColorMode";
diff --git a/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/useColorMode.tsx b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/useColorMode.tsx
new file mode 100644
index 00000000000..3cbca6b5f2e
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/useColorMode.tsx
@@ -0,0 +1,33 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useTheme } from "next-themes";
+
+export const useColorMode = () => {
+  const { resolvedTheme, setTheme } = useTheme();
+  const toggleColorMode = () => {
+    setTheme(resolvedTheme === "light" ? "dark" : "light");
+  };
+
+  return {
+    colorMode: resolvedTheme as "dark" | "light" | undefined,
+    setColorMode: setTheme,
+    toggleColorMode,
+  };
+};
diff --git a/dev/react-plugin-tools/react_plugin_template/src/dev.tsx b/dev/react-plugin-tools/react_plugin_template/src/dev.tsx
new file mode 100644
index 00000000000..76b2b0016fd
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/dev.tsx
@@ -0,0 +1,30 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import PluginComponent from "./main";
+
+// Development entry point for testing the component
+createRoot(document.querySelector("#root") as HTMLDivElement).render(
+  <StrictMode>
+    <PluginComponent />
+  </StrictMode>,
+);
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/dev/react-plugin-tools/react_plugin_template/src/main.tsx
similarity index 50%
copy from airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
copy to dev/react-plugin-tools/react_plugin_template/src/main.tsx
index 0f80af777c8..8ee670c0dac 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/dev/react-plugin-tools/react_plugin_template/src/main.tsx
@@ -16,31 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Spinner } from "@chakra-ui/react";
-import { lazy, Suspense } from "react";
-import { useParams } from "react-router-dom";
 
-import type { ReactAppResponse } from "openapi/requests/types.gen";
+import { ChakraProvider } from "@chakra-ui/react";
+import { FC } from "react";
 
-import { ErrorPage } from "./Error";
+import { ColorModeProvider } from "src/context/colorMode";
+import { HomePage } from "src/pages/HomePage";
 
-export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse }) => {
-  const { dagId, mapIndex, runId, taskId } = useParams();
+import { system } from "./theme";
 
-  const Plugin = lazy(() =>
-    // We are assuming the plugin manager is trusted and the bundle_url is safe
-    import(/* @vite-ignore */ reactApp.bundle_url).catch((error: unknown) => {
-      console.error("Component Failed Loading:", error);
-
-      return {
-        default: <ErrorPage />,
-      };
-    }),
-  );
+export interface PluginComponentProps {
+  // Add any props your plugin component needs
+}
 
+/**
+ * Main plugin component
+ */
+const PluginComponent: FC<PluginComponentProps> = (props) => {
   return (
-    <Suspense fallback={<Spinner />}>
-      <Plugin dagId={dagId} mapIndex={mapIndex} runId={runId} taskId={taskId} />
-    </Suspense>
+    <ChakraProvider value={system}>
+      <ColorModeProvider>
+          <HomePage />
+      </ColorModeProvider>
+    </ChakraProvider>
   );
 };
+
+export default PluginComponent;
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/dev/react-plugin-tools/react_plugin_template/src/pages/HomePage.tsx
similarity index 50%
copy from airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
copy to dev/react-plugin-tools/react_plugin_template/src/pages/HomePage.tsx
index 0f80af777c8..3b90631de3d 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/dev/react-plugin-tools/react_plugin_template/src/pages/HomePage.tsx
@@ -16,31 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Spinner } from "@chakra-ui/react";
-import { lazy, Suspense } from "react";
-import { useParams } from "react-router-dom";
 
-import type { ReactAppResponse } from "openapi/requests/types.gen";
+import { Box, Button, Heading, Text, VStack } from "@chakra-ui/react";
 
-import { ErrorPage } from "./Error";
+import { useColorMode } from "src/context/colorMode";
 
-export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse }) => {
-  const { dagId, mapIndex, runId, taskId } = useParams();
-
-  const Plugin = lazy(() =>
-    // We are assuming the plugin manager is trusted and the bundle_url is safe
-    import(/* @vite-ignore */ reactApp.bundle_url).catch((error: unknown) => {
-      console.error("Component Failed Loading:", error);
-
-      return {
-        default: <ErrorPage />,
-      };
-    }),
-  );
+export const HomePage = () => {
+  const { toggleColorMode } = useColorMode();
 
   return (
-    <Suspense fallback={<Spinner />}>
-      <Plugin dagId={dagId} mapIndex={mapIndex} runId={runId} taskId={taskId} />
-    </Suspense>
+    <Box p={8} bg="bg.subtle" flexGrow={1} height="100%">
+      <VStack gap={8} align="center" justify="center" flexGrow={1} height="100%">
+        <Heading size="2xl" textAlign="center" color="fg">
+          Welcome to Your New React App!
+        </Heading>
+        <Text fontSize="lg" color="fg.muted">
+          This project was bootstrapped with the Airflow React Plugin tool.
+        </Text>
+        <Button onClick={toggleColorMode} colorPalette="blue">
+          Toggle Theme
+        </Button>
+      </VStack>
+    </Box>
   );
 };
diff --git a/dev/react-plugin-tools/react_plugin_template/src/theme.ts b/dev/react-plugin-tools/react_plugin_template/src/theme.ts
new file mode 100644
index 00000000000..706ae0ed57e
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/theme.ts
@@ -0,0 +1,176 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* eslint-disable perfectionist/sort-objects */
+import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react";
+
+const generateSemanticTokens = (color: string, darkContrast: string = "white") => ({
+  solid: { value: `{colors.${color}.600}` },
+  contrast: { value: { _light: "white", _dark: darkContrast } },
+  fg: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } },
+  muted: { value: { _light: `{colors.${color}.200}`, _dark: `{colors.${color}.800}` } },
+  subtle: { value: { _light: `{colors.${color}.100}`, _dark: `{colors.${color}.900}` } },
+  emphasized: { value: { _light: `{colors.${color}.300}`, _dark: `{colors.${color}.700}` } },
+  focusRing: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } },
+});
+
+const customConfig = defineConfig({
+  theme: {
+    tokens: {
+      colors: {
+        // Default green was too light
+        success: {
+          "50": { value: "#E0FFE0" },
+          "100": { value: "#C2FFC2" },
+          "200": { value: "#80FF80" },
+          "300": { value: "#42FF42" },
+          "400": { value: "#00FF00" },
+          "500": { value: "#00C200" },
+          "600": { value: "#008000" },
+          "700": { value: "#006100" },
+          "800": { value: "#004200" },
+          "900": { value: "#001F00" },
+          "950": { value: "#000F00" },
+        },
+        failed: defaultConfig.theme?.tokens?.colors?.red ?? {},
+        // Default gray was too dark
+        queued: {
+          "50": { value: "#F5F5F5" },
+          "100": { value: "#EBEBEB" },
+          "200": { value: "#D4D4D4" },
+          "300": { value: "#BFBFBF" },
+          "400": { value: "#ABABAB" },
+          "500": { value: "#969696" },
+          "600": { value: "#808080" },
+          "700": { value: "#616161" },
+          "800": { value: "#404040" },
+          "900": { value: "#212121" },
+          "950": { value: "#0F0F0F" },
+        },
+        skipped: defaultConfig.theme?.tokens?.colors?.pink ?? {},
+        up_for_reschedule: defaultConfig.theme?.tokens?.colors?.cyan ?? {},
+        up_for_retry: defaultConfig.theme?.tokens?.colors?.yellow ?? {},
+        upstream_failed: defaultConfig.theme?.tokens?.colors?.orange ?? {},
+        // lime
+        running: {
+          "50": { value: "#EFFBEF" },
+          "100": { value: "#DEF7DE" },
+          "200": { value: "#B9EEB9" },
+          "300": { value: "#98E698" },
+          "400": { value: "#78DE78" },
+          "500": { value: "#53D553" },
+          "600": { value: "#32CD32" },
+          "700": { value: "#269C26" },
+          "800": { value: "#196719" },
+          "900": { value: "#0D350D" },
+          "950": { value: "#061906" },
+        },
+        // violet
+        restarting: {
+          "50": { value: "#F6EBFF" },
+          "100": { value: "#EDD6FF" },
+          "200": { value: "#D9A8FF" },
+          "300": { value: "#C880FF" },
+          "400": { value: "#B657FF" },
+          "500": { value: "#A229FF" },
+          "600": { value: "#8F00FF" },
+          "700": { value: "#6E00C2" },
+          "800": { value: "#480080" },
+          "900": { value: "#260042" },
+          "950": { value: "#11001F" },
+        },
+        // mediumpurple
+        deferred: {
+          "50": { value: "#F6F3FC" },
+          "100": { value: "#EDE7F9" },
+          "200": { value: "#DACEF3" },
+          "300": { value: "#C8B6ED" },
+          "400": { value: "#B9A1E7" },
+          "500": { value: "#A689E1" },
+          "600": { value: "#9370DB" },
+          "700": { value: "#6432C8" },
+          "800": { value: "#412182" },
+          "900": { value: "#211041" },
+          "950": { value: "#100821" },
+        },
+        // tan
+        scheduled: {
+          "50": { value: "#FBF8F4" },
+          "100": { value: "#F8F3ED" },
+          "200": { value: "#F1E7DA" },
+          "300": { value: "#E8D9C4" },
+          "400": { value: "#E1CDB2" },
+          "500": { value: "#DAC1A0" },
+          "600": { value: "#D2B48C" },
+          "700": { value: "#B9894B" },
+          "800": { value: "#7D5C31" },
+          "900": { value: "#3E2E18" },
+          "950": { value: "#21180D" },
+        },
+        // lightblue
+        none: {
+          "50": { value: "#F7FBFD" },
+          "100": { value: "#F3F9FB" },
+          "200": { value: "#E4F2F7" },
+          "300": { value: "#D8ECF3" },
+          "400": { value: "#C8E5EE" },
+          "500": { value: "#BDDFEB" },
+          "600": { value: "#ADD8E6" },
+          "700": { value: "#5FB2CE" },
+          "800": { value: "#30819C" },
+          "900": { value: "#18414E" },
+          "950": { value: "#0C2027" },
+        },
+        // lightgrey
+        removed: {
+          "50": { value: "#FCFCFC" },
+          "100": { value: "#F7F7F7" },
+          "200": { value: "#F0F0F0" },
+          "300": { value: "#E8E8E8" },
+          "400": { value: "#E0E0E0" },
+          "500": { value: "#DBDBDB" },
+          "600": { value: "#D3D3D3" },
+          "700": { value: "#9E9E9E" },
+          "800": { value: "#696969" },
+          "900": { value: "#363636" },
+          "950": { value: "#1A1A1A" },
+        },
+      },
+    },
+    semanticTokens: {
+      colors: {
+        success: generateSemanticTokens("success"),
+        failed: defaultConfig.theme?.semanticTokens?.colors?.red ?? {},
+        queued: generateSemanticTokens("queued"),
+        skipped: defaultConfig.theme?.semanticTokens?.colors?.pink ?? {},
+        up_for_reschedule: defaultConfig.theme?.semanticTokens?.colors?.cyan ?? {},
+        up_for_retry: defaultConfig.theme?.semanticTokens?.colors?.yellow ?? {},
+        upstream_failed: defaultConfig.theme?.semanticTokens?.colors?.orange ?? {},
+        running: generateSemanticTokens("running"),
+        restarting: generateSemanticTokens("restarting"),
+        deferred: generateSemanticTokens("deferred"),
+        scheduled: generateSemanticTokens("scheduled"),
+        none: generateSemanticTokens("none", "black"),
+        removed: generateSemanticTokens("removed", "black"),
+      },
+    },
+  },
+});
+
+export const system = createSystem(defaultConfig, customConfig);
diff --git a/dev/react-plugin-tools/react_plugin_template/src/vite-env.d.ts b/dev/react-plugin-tools/react_plugin_template/src/vite-env.d.ts
new file mode 100644
index 00000000000..a1fdcdd1e6f
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/vite-env.d.ts
@@ -0,0 +1,20 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// <reference types="vite/client" />
diff --git a/dev/react-plugin-tools/react_plugin_template/testsSetup.ts b/dev/react-plugin-tools/react_plugin_template/testsSetup.ts
new file mode 100644
index 00000000000..c060cb550be
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/testsSetup.ts
@@ -0,0 +1,20 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import "@testing-library/jest-dom/vitest";
diff --git a/airflow-core/src/airflow/ui/tsconfig.node.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.app.json
similarity index 72%
copy from airflow-core/src/airflow/ui/tsconfig.node.json
copy to dev/react-plugin-tools/react_plugin_template/tsconfig.app.json
index a731173fc6c..d91d91e9622 100644
--- a/airflow-core/src/airflow/ui/tsconfig.node.json
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.app.json
@@ -2,7 +2,8 @@
   "$schema": "https://json.schemastore.org/tsconfig",
   "compilerOptions": {
     "target": "ES2022",
-    "lib": ["ES2023"],
+    "useDefineForClassFields": true,
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
     "module": "ESNext",
     "skipLibCheck": true,
 
@@ -12,17 +13,18 @@
     "isolatedModules": true,
     "moduleDetection": "force",
     "noEmit": true,
+    "jsx": "react-jsx",
 
     /* Linting */
     "strict": true,
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noFallthroughCasesInSwitch": true,
+    "noUncheckedIndexedAccess": true,
     "baseUrl": ".",
     "paths": {
-      "src/*": ["./src/*"],
-      "openapi/*": ["./openapi-gen/*"]
+      "src/*": ["./src/*"]
     }
   },
-  "include": ["vite.config.ts"]
+  "include": ["src"]
 }
diff --git a/dev/react-plugin-tools/react_plugin_template/tsconfig.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.json
new file mode 100644
index 00000000000..b6a01bf3b76
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" },
+    { "path": "./tsconfig.lib.json" }
+  ]
+}
diff --git a/dev/react-plugin-tools/react_plugin_template/tsconfig.lib.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.lib.json
new file mode 100644
index 00000000000..d423df60f83
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.lib.json
@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "./tsconfig.app.json",
+  "compilerOptions": {
+    "declaration": true,
+    "declarationMap": true,
+    "emitDeclarationOnly": true,
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "noEmit": false,
+    "allowImportingTsExtensions": false
+  },
+  "include": ["src/main.tsx"],
+  "exclude": ["**/*.test.*", "**/*.spec.*"]
+}
diff --git a/airflow-core/src/airflow/ui/tsconfig.node.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.node.json
similarity index 89%
copy from airflow-core/src/airflow/ui/tsconfig.node.json
copy to dev/react-plugin-tools/react_plugin_template/tsconfig.node.json
index a731173fc6c..9cd36cf34db 100644
--- a/airflow-core/src/airflow/ui/tsconfig.node.json
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.node.json
@@ -18,10 +18,10 @@
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noFallthroughCasesInSwitch": true,
+    "noUncheckedIndexedAccess": true,
     "baseUrl": ".",
     "paths": {
-      "src/*": ["./src/*"],
-      "openapi/*": ["./openapi-gen/*"]
+      "src/*": ["./src/*"]
     }
   },
   "include": ["vite.config.ts"]
diff --git a/dev/react-plugin-tools/react_plugin_template/vite.config.ts b/dev/react-plugin-tools/react_plugin_template/vite.config.ts
new file mode 100644
index 00000000000..dc9384ff2bc
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/vite.config.ts
@@ -0,0 +1,84 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import react from "@vitejs/plugin-react-swc";
+import { resolve } from "node:path";
+import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
+import dts from "vite-plugin-dts";
+import { defineConfig } from "vitest/config";
+
+// https://vitejs.dev/config/
+export default defineConfig(({ command }) => {
+  const isLibraryBuild = command === 'build';
+
+  return {
+    base: "./",
+    build: isLibraryBuild ? {
+      chunkSizeWarningLimit: 1600,
+      lib: {
+        entry: resolve("src", "main.tsx"),
+        fileName: 'main',
+        formats: ['umd'],
+        name: 'AirflowPlugin',
+      },
+      rollupOptions: {
+        external: ["react", "react-dom"],
+        output: {
+          globals: {
+            react: "React",
+            "react/jsx-runtime": "ReactJSXRuntime",
+          },
+        },
+      },
+    } : {
+      // Development build configuration
+      chunkSizeWarningLimit: 1600
+    },
+    define: {
+      global: "globalThis",
+      "process.env": "{}",
+      // Define process.env for browser compatibility
+      "process.env.NODE_ENV": JSON.stringify("production"),
+    },
+    plugins: [
+      react(),
+      cssInjectedByJsPlugin(),
+      ...(isLibraryBuild ? [dts({
+        include: ["src/main.tsx"],
+        insertTypesEntry: true,
+        outDir: "dist"
+      })] : [])
+    ],
+    resolve: { alias: { src: "/src" } },
+    server: {
+      cors: true, // Only used by the dev server.
+    },
+    test: {
+      coverage: {
+        include: ["src/**/*.ts", "src/**/*.tsx"],
+      },
+      css: true,
+      environment: "happy-dom",
+      globals: true,
+      mockReset: true,
+      restoreMocks: true,
+      setupFiles: "./testsSetup.ts",
+    },
+  };
+});