|
|
#!/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()
|