Unused import A module is imported (using the import statement) but that module is never used. This creates a dependency that does not need to exist and makes the code more difficult to read. Recommendation Delete the import statement.

This commit is contained in:
2026-01-17 17:21:10 -06:00
parent 1fdf8ba2d0
commit f70b702b76
4 changed files with 789 additions and 793 deletions

View File

@@ -32,210 +32,208 @@ USAGE: ./scripts/release/package_extension.py [output_dir] [version]
""" """
import argparse import argparse
import os
import shutil
import sys import sys
import zipfile import zipfile
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List, Set from typing import Set
# Add lib directory to path # Add lib directory to path
sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
try: try:
import common import common
import extension_utils import extension_utils
except ImportError: except ImportError:
print("ERROR: Cannot import required libraries", file=sys.stderr) print("ERROR: Cannot import required libraries", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Exclusion patterns for packaging # Exclusion patterns for packaging
EXCLUDE_PATTERNS = { EXCLUDE_PATTERNS = {
# Version control # Version control
".git", ".gitignore", ".gitattributes", ".git", ".gitignore", ".gitattributes",
# IDE # IDE
".vscode", ".idea", "*.sublime-*", ".vscode", ".idea", "*.sublime-*",
# Development # Development
"node_modules", "vendor", ".env", ".env.*", "node_modules", "vendor", ".env", ".env.*",
# Documentation (optional, can be included) # Documentation (optional, can be included)
# Build artifacts # Build artifacts
"dist", "build", ".phpunit.cache", "dist", "build", ".phpunit.cache",
# Development tool caches and artifacts # Development tool caches and artifacts
".phpstan.cache", ".psalm", ".rector", ".phpstan.cache", ".psalm", ".rector",
"phpmd-cache", ".php-cs-fixer.cache", ".phplint-cache", "phpmd-cache", ".php-cs-fixer.cache", ".phplint-cache",
# OS files # OS files
".DS_Store", "Thumbs.db", ".DS_Store", "Thumbs.db",
# Logs # Logs
"*.log", "*.log",
# Tests # Tests
"tests", "test", "Tests", "tests", "test", "Tests",
# CI/CD # CI/CD
".github", ".github",
# Scripts # Scripts
"scripts", "scripts",
# Docs (can be included if needed) # Docs (can be included if needed)
"docs", "docs",
# Config files # Config files
"composer.json", "composer.lock", "composer.json", "composer.lock",
"package.json", "package-lock.json", "package.json", "package-lock.json",
"phpunit.xml", "phpstan.neon", "phpcs.xml", "phpunit.xml", "phpstan.neon", "phpcs.xml",
"codeception.yml", "psalm.xml", ".php-cs-fixer.php", "codeception.yml", "psalm.xml", ".php-cs-fixer.php",
# Others # Others
"README.md", "CHANGELOG.md", "CONTRIBUTING.md", "README.md", "CHANGELOG.md", "CONTRIBUTING.md",
"CODE_OF_CONDUCT.md", "SECURITY.md", "GOVERNANCE.md", "CODE_OF_CONDUCT.md", "SECURITY.md", "GOVERNANCE.md",
"Makefile", "Makefile",
} }
def should_exclude(path: Path, base_path: Path, exclude_patterns: Set[str]) -> bool: def should_exclude(path: Path, base_path: Path, exclude_patterns: Set[str]) -> bool:
""" """
Check if a path should be excluded from packaging. Check if a path should be excluded from packaging.
Args: Args:
path: Path to check path: Path to check
base_path: Base directory path base_path: Base directory path
exclude_patterns: Set of exclusion patterns exclude_patterns: Set of exclusion patterns
Returns: Returns:
True if should be excluded True if should be excluded
""" """
relative_path = path.relative_to(base_path) relative_path = path.relative_to(base_path)
# Check each part of the path # Check each part of the path
for part in relative_path.parts: for part in relative_path.parts:
if part in exclude_patterns: if part in exclude_patterns:
return True return True
# Check wildcard patterns # Check wildcard patterns
for pattern in exclude_patterns: for pattern in exclude_patterns:
if "*" in pattern: if "*" in pattern:
import fnmatch import fnmatch
if fnmatch.fnmatch(part, pattern): if fnmatch.fnmatch(part, pattern):
return True return True
return False return False
def create_package( def create_package(
src_dir: str, src_dir: str,
output_dir: str, output_dir: str,
version: str = None, version: str = None,
repo_name: str = None, repo_name: str = None,
exclude_patterns: Set[str] = None exclude_patterns: Set[str] = None
) -> Path: ) -> Path:
""" """
Create a distributable ZIP package for a Joomla or Dolibarr extension. Create a distributable ZIP package for a Joomla or Dolibarr extension.
Args: Args:
src_dir: Source directory containing extension files src_dir: Source directory containing extension files
output_dir: Output directory for ZIP file output_dir: Output directory for ZIP file
version: Version string (auto-detected if not provided) version: Version string (auto-detected if not provided)
repo_name: Repository name for ZIP file naming repo_name: Repository name for ZIP file naming
exclude_patterns: Patterns to exclude from packaging exclude_patterns: Patterns to exclude from packaging
Returns: Returns:
Path to created ZIP file Path to created ZIP file
""" """
src_path = Path(src_dir) src_path = Path(src_dir)
if not src_path.is_dir(): if not src_path.is_dir():
common.die(f"Source directory not found: {src_dir}") common.die(f"Source directory not found: {src_dir}")
# Detect extension platform and get info # Detect extension platform and get info
ext_info = extension_utils.get_extension_info(src_dir) ext_info = extension_utils.get_extension_info(src_dir)
if not ext_info: if not ext_info:
common.die(f"No Joomla or Dolibarr extension found in {src_dir}") common.die(f"No Joomla or Dolibarr extension found in {src_dir}")
# Determine version # Determine version
if not version: if not version:
version = ext_info.version version = ext_info.version
# Determine repo name # Determine repo name
if not repo_name: if not repo_name:
try: try:
repo_root = common.git_root() repo_root = common.git_root()
repo_name = repo_root.name repo_name = repo_root.name
except Exception: except Exception:
repo_name = "extension" repo_name = "extension"
# Determine exclusion patterns # Determine exclusion patterns
if exclude_patterns is None: if exclude_patterns is None:
exclude_patterns = EXCLUDE_PATTERNS exclude_patterns = EXCLUDE_PATTERNS
# Create output directory # Create output directory
output_path = Path(output_dir) output_path = Path(output_dir)
common.ensure_dir(output_path) common.ensure_dir(output_path)
# Generate ZIP filename # Generate ZIP filename
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
platform_suffix = f"{ext_info.platform.value}-{ext_info.extension_type}" platform_suffix = f"{ext_info.platform.value}-{ext_info.extension_type}"
zip_filename = f"{repo_name}-{version}-{platform_suffix}.zip" zip_filename = f"{repo_name}-{version}-{platform_suffix}.zip"
zip_path = output_path / zip_filename zip_path = output_path / zip_filename
# Remove existing ZIP if present # Remove existing ZIP if present
if zip_path.exists(): if zip_path.exists():
zip_path.unlink() zip_path.unlink()
common.log_section("Creating Extension Package") common.log_section("Creating Extension Package")
common.log_kv("Platform", ext_info.platform.value.upper()) common.log_kv("Platform", ext_info.platform.value.upper())
common.log_kv("Extension", ext_info.name) common.log_kv("Extension", ext_info.name)
common.log_kv("Type", ext_info.extension_type) common.log_kv("Type", ext_info.extension_type)
common.log_kv("Version", version) common.log_kv("Version", version)
common.log_kv("Source", src_dir) common.log_kv("Source", src_dir)
common.log_kv("Output", str(zip_path)) common.log_kv("Output", str(zip_path))
print() print()
# Create ZIP file # Create ZIP file
file_count = 0 file_count = 0
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for item in src_path.rglob("*"): for item in src_path.rglob("*"):
if item.is_file(): if item.is_file():
# Check if should be excluded # Check if should be excluded
if should_exclude(item, src_path, exclude_patterns): if should_exclude(item, src_path, exclude_patterns):
continue continue
# Add to ZIP with relative path # Add to ZIP with relative path
arcname = item.relative_to(src_path) arcname = item.relative_to(src_path)
zipf.write(item, arcname) zipf.write(item, arcname)
file_count += 1 file_count += 1
if file_count % 10 == 0: if file_count % 10 == 0:
common.log_step(f"Added {file_count} files...") common.log_step(f"Added {file_count} files...")
# Get ZIP file size # Get ZIP file size
zip_size = zip_path.stat().st_size zip_size = zip_path.stat().st_size
zip_size_mb = zip_size / (1024 * 1024) zip_size_mb = zip_size / (1024 * 1024)
print() print()
common.log_success(f"Package created: {zip_path.name}") common.log_success(f"Package created: {zip_path.name}")
common.log_kv("Files", str(file_count)) common.log_kv("Files", str(file_count))
common.log_kv("Size", f"{zip_size_mb:.2f} MB") common.log_kv("Size", f"{zip_size_mb:.2f} MB")
# Output JSON for machine consumption # Output JSON for machine consumption
result = { result = {
"status": "ok", "status": "ok",
"platform": ext_info.platform.value, "platform": ext_info.platform.value,
"extension": ext_info.name, "extension": ext_info.name,
"ext_type": ext_info.extension_type, "ext_type": ext_info.extension_type,
"version": version, "version": version,
"package": str(zip_path), "package": str(zip_path),
"files": file_count, "files": file_count,
"size_bytes": zip_size "size_bytes": zip_size
} }
print() print()
common.json_output(result) common.json_output(result)
return zip_path return zip_path
def main() -> int: def main() -> int:
"""Main entry point.""" """Main entry point."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Package Joomla or Dolibarr extension as distributable ZIP", description="Package Joomla or Dolibarr extension as distributable ZIP",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples:
# Package with auto-detected version # Package with auto-detected version
%(prog)s %(prog)s
@@ -251,73 +249,73 @@ Examples:
Supports both Joomla and Dolibarr extensions with automatic platform detection. Supports both Joomla and Dolibarr extensions with automatic platform detection.
""" """
) )
parser.add_argument( parser.add_argument(
"output_dir", "output_dir",
nargs="?", nargs="?",
default="dist", default="dist",
help="Output directory for ZIP file (default: dist)" help="Output directory for ZIP file (default: dist)"
) )
parser.add_argument( parser.add_argument(
"version", "version",
nargs="?", nargs="?",
help="Version string (default: auto-detected from manifest)" help="Version string (default: auto-detected from manifest)"
) )
parser.add_argument( parser.add_argument(
"-s", "--src-dir", "-s", "--src-dir",
default="src", default="src",
help="Source directory (default: src)" help="Source directory (default: src)"
) )
parser.add_argument( parser.add_argument(
"--repo-name", "--repo-name",
help="Repository name for ZIP filename (default: auto-detected)" help="Repository name for ZIP filename (default: auto-detected)"
) )
parser.add_argument( parser.add_argument(
"--include-docs", "--include-docs",
action="store_true", action="store_true",
help="Include documentation files in package" help="Include documentation files in package"
) )
parser.add_argument( parser.add_argument(
"--include-tests", "--include-tests",
action="store_true", action="store_true",
help="Include test files in package" help="Include test files in package"
) )
args = parser.parse_args() args = parser.parse_args()
try: try:
# Adjust exclusion patterns based on arguments # Adjust exclusion patterns based on arguments
exclude_patterns = EXCLUDE_PATTERNS.copy() exclude_patterns = EXCLUDE_PATTERNS.copy()
if args.include_docs: if args.include_docs:
exclude_patterns.discard("docs") exclude_patterns.discard("docs")
exclude_patterns.discard("README.md") exclude_patterns.discard("README.md")
exclude_patterns.discard("CHANGELOG.md") exclude_patterns.discard("CHANGELOG.md")
if args.include_tests: if args.include_tests:
exclude_patterns.discard("tests") exclude_patterns.discard("tests")
exclude_patterns.discard("test") exclude_patterns.discard("test")
exclude_patterns.discard("Tests") exclude_patterns.discard("Tests")
# Create package # Create package
zip_path = create_package( zip_path = create_package(
src_dir=args.src_dir, src_dir=args.src_dir,
output_dir=args.output_dir, output_dir=args.output_dir,
version=args.version, version=args.version,
repo_name=args.repo_name, repo_name=args.repo_name,
exclude_patterns=exclude_patterns exclude_patterns=exclude_patterns
) )
return 0 return 0
except Exception as e: except Exception as e:
common.log_error(f"Packaging failed: {e}") common.log_error(f"Packaging failed: {e}")
result = { result = {
"status": "error", "status": "error",
"error": str(e) "error": str(e)
} }
common.json_output(result) common.json_output(result)
return 1 return 1
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@@ -40,11 +40,10 @@ from typing import Dict
sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
try: try:
import common import common
import joomla_manifest
except ImportError: except ImportError:
print("ERROR: Cannot import required libraries", file=sys.stderr) print("ERROR: Cannot import required libraries", file=sys.stderr)
sys.exit(1) sys.exit(1)
# ============================================================================ # ============================================================================
@@ -52,73 +51,73 @@ except ImportError:
# ============================================================================ # ============================================================================
def get_component_structure(name: str, description: str, author: str) -> Dict[str, str]: def get_component_structure(name: str, description: str, author: str) -> Dict[str, str]:
"""Get directory structure and files for a component.""" """Get directory structure and files for a component."""
safe_name = name.lower().replace(" ", "_") safe_name = name.lower().replace(" ", "_")
com_name = f"com_{safe_name}" com_name = f"com_{safe_name}"
manifest = f"""<?xml version="1.0" encoding="utf-8"?> manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="4.0" method="upgrade"> <extension type="component" version="4.0" method="upgrade">
<name>{name}</name> <name>{name}</name>
<author>{author}</author> <author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate> <creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright> <copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail> <authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl> <authorUrl>https://example.com</authorUrl>
<version>1.0.0</version> <version>1.0.0</version>
<description>{description}</description> <description>{description}</description>
<files folder="site"> <files folder="site">
<folder>src</folder> <folder>src</folder>
</files> </files>
<administration> <administration>
<menu>{name}</menu> <menu>{name}</menu>
<files folder="admin"> <files folder="admin">
<folder>services</folder> <folder>services</folder>
<folder>sql</folder> <folder>sql</folder>
<folder>src</folder> <folder>src</folder>
</files> </files>
</administration> </administration>
</extension> </extension>
""" """
return { return {
f"{com_name}.xml": manifest, f"{com_name}.xml": manifest,
"site/src/.gitkeep": "", "site/src/.gitkeep": "",
"admin/services/provider.php": f"<?php\n// Service provider for {name}\n", "admin/services/provider.php": f"<?php\n// Service provider for {name}\n",
"admin/sql/install.mysql.utf8.sql": "-- Installation SQL\n", "admin/sql/install.mysql.utf8.sql": "-- Installation SQL\n",
"admin/sql/uninstall.mysql.utf8.sql": "-- Uninstallation SQL\n", "admin/sql/uninstall.mysql.utf8.sql": "-- Uninstallation SQL\n",
"admin/src/.gitkeep": "", "admin/src/.gitkeep": "",
} }
def get_module_structure(name: str, description: str, author: str, client: str = "site") -> Dict[str, str]: def get_module_structure(name: str, description: str, author: str, client: str = "site") -> Dict[str, str]:
"""Get directory structure and files for a module.""" """Get directory structure and files for a module."""
safe_name = name.lower().replace(" ", "_") safe_name = name.lower().replace(" ", "_")
mod_name = f"mod_{safe_name}" mod_name = f"mod_{safe_name}"
manifest = f"""<?xml version="1.0" encoding="utf-8"?> manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="module" version="4.0" client="{client}" method="upgrade"> <extension type="module" version="4.0" client="{client}" method="upgrade">
<name>{name}</name> <name>{name}</name>
<author>{author}</author> <author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate> <creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright> <copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail> <authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl> <authorUrl>https://example.com</authorUrl>
<version>1.0.0</version> <version>1.0.0</version>
<description>{description}</description> <description>{description}</description>
<files> <files>
<filename module="{mod_name}">{mod_name}.php</filename> <filename module="{mod_name}">{mod_name}.php</filename>
<filename>{mod_name}.xml</filename> <filename>{mod_name}.xml</filename>
<folder>tmpl</folder> <folder>tmpl</folder>
</files> </files>
</extension> </extension>
""" """
module_php = f"""<?php module_php = f"""<?php
/** /**
* @package {name} * @package {name}
* @copyright Copyright (C) {datetime.now().year} {author} * @copyright Copyright (C) {datetime.now().year} {author}
@@ -131,7 +130,7 @@ defined('_JEXEC') or die;
require JModuleHelper::getLayoutPath('mod_{safe_name}', $params->get('layout', 'default')); require JModuleHelper::getLayoutPath('mod_{safe_name}', $params->get('layout', 'default'));
""" """
default_tmpl = f"""<?php default_tmpl = f"""<?php
/** /**
* @package {name} * @package {name}
* @copyright Copyright (C) {datetime.now().year} {author} * @copyright Copyright (C) {datetime.now().year} {author}
@@ -141,41 +140,41 @@ require JModuleHelper::getLayoutPath('mod_{safe_name}', $params->get('layout', '
defined('_JEXEC') or die; defined('_JEXEC') or die;
?> ?>
<div class="{mod_name}"> <div class="{mod_name}">
<p><?php echo JText::_('MOD_{safe_name.upper()}_DESCRIPTION'); ?></p> <p><?php echo JText::_('MOD_{safe_name.upper()}_DESCRIPTION'); ?></p>
</div> </div>
""" """
return { return {
f"{mod_name}.xml": manifest, f"{mod_name}.xml": manifest,
f"{mod_name}.php": module_php, f"{mod_name}.php": module_php,
"tmpl/default.php": default_tmpl, "tmpl/default.php": default_tmpl,
} }
def get_plugin_structure(name: str, description: str, author: str, group: str = "system") -> Dict[str, str]: def get_plugin_structure(name: str, description: str, author: str, group: str = "system") -> Dict[str, str]:
"""Get directory structure and files for a plugin.""" """Get directory structure and files for a plugin."""
safe_name = name.lower().replace(" ", "_") safe_name = name.lower().replace(" ", "_")
plg_name = f"{safe_name}" plg_name = f"{safe_name}"
manifest = f"""<?xml version="1.0" encoding="utf-8"?> manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" version="4.0" group="{group}" method="upgrade"> <extension type="plugin" version="4.0" group="{group}" method="upgrade">
<name>plg_{group}_{safe_name}</name> <name>plg_{group}_{safe_name}</name>
<author>{author}</author> <author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate> <creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright> <copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail> <authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl> <authorUrl>https://example.com</authorUrl>
<version>1.0.0</version> <version>1.0.0</version>
<description>{description}</description> <description>{description}</description>
<files> <files>
<filename plugin="{plg_name}">{plg_name}.php</filename> <filename plugin="{plg_name}">{plg_name}.php</filename>
</files> </files>
</extension> </extension>
""" """
plugin_php = f"""<?php plugin_php = f"""<?php
/** /**
* @package {name} * @package {name}
* @copyright Copyright (C) {datetime.now().year} {author} * @copyright Copyright (C) {datetime.now().year} {author}
@@ -188,54 +187,54 @@ use Joomla\\CMS\\Plugin\\CMSPlugin;
class Plg{group.capitalize()}{plg_name.capitalize()} extends CMSPlugin class Plg{group.capitalize()}{plg_name.capitalize()} extends CMSPlugin
{{ {{
protected $autoloadLanguage = true; protected $autoloadLanguage = true;
public function onContentPrepare($context, &$article, &$params, $limitstart = 0) public function onContentPrepare($context, &$article, &$params, $limitstart = 0)
{{ {{
// Plugin logic here // Plugin logic here
}} }}
}} }}
""" """
return { return {
f"plg_{group}_{safe_name}.xml": manifest, f"plg_{group}_{safe_name}.xml": manifest,
f"{plg_name}.php": plugin_php, f"{plg_name}.php": plugin_php,
} }
def get_template_structure(name: str, description: str, author: str) -> Dict[str, str]: def get_template_structure(name: str, description: str, author: str) -> Dict[str, str]:
"""Get directory structure and files for a template.""" """Get directory structure and files for a template."""
safe_name = name.lower().replace(" ", "_") safe_name = name.lower().replace(" ", "_")
manifest = f"""<?xml version="1.0" encoding="utf-8"?> manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="template" version="4.0" client="site" method="upgrade"> <extension type="template" version="4.0" client="site" method="upgrade">
<name>{safe_name}</name> <name>{safe_name}</name>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate> <creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<author>{author}</author> <author>{author}</author>
<authorEmail>hello@example.com</authorEmail> <authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl> <authorUrl>https://example.com</authorUrl>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright> <copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<version>1.0.0</version> <version>1.0.0</version>
<description>{description}</description> <description>{description}</description>
<files> <files>
<filename>index.php</filename> <filename>index.php</filename>
<filename>templateDetails.xml</filename> <filename>templateDetails.xml</filename>
<folder>css</folder> <folder>css</folder>
<folder>js</folder> <folder>js</folder>
<folder>images</folder> <folder>images</folder>
</files> </files>
<positions> <positions>
<position>header</position> <position>header</position>
<position>main</position> <position>main</position>
<position>footer</position> <position>footer</position>
</positions> </positions>
</extension> </extension>
""" """
index_php = f"""<?php index_php = f"""<?php
/** /**
* @package {name} * @package {name}
* @copyright Copyright (C) {datetime.now().year} {author} * @copyright Copyright (C) {datetime.now().year} {author}
@@ -257,61 +256,61 @@ $wa->useStyle('template.{safe_name}')->useScript('template.{safe_name}');
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>"> <html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head> <head>
<jdoc:include type="metas" /> <jdoc:include type="metas" />
<jdoc:include type="styles" /> <jdoc:include type="styles" />
<jdoc:include type="scripts" /> <jdoc:include type="scripts" />
</head> </head>
<body> <body>
<header> <header>
<jdoc:include type="modules" name="header" style="html5" /> <jdoc:include type="modules" name="header" style="html5" />
</header> </header>
<main> <main>
<jdoc:include type="component" /> <jdoc:include type="component" />
</main> </main>
<footer> <footer>
<jdoc:include type="modules" name="footer" style="html5" /> <jdoc:include type="modules" name="footer" style="html5" />
</footer> </footer>
</body> </body>
</html> </html>
""" """
return { return {
"templateDetails.xml": manifest, "templateDetails.xml": manifest,
"index.php": index_php, "index.php": index_php,
"css/template.css": "/* Template styles */\n", "css/template.css": "/* Template styles */\n",
"js/template.js": "// Template JavaScript\n", "js/template.js": "// Template JavaScript\n",
"images/.gitkeep": "", "images/.gitkeep": "",
} }
def get_package_structure(name: str, description: str, author: str) -> Dict[str, str]: def get_package_structure(name: str, description: str, author: str) -> Dict[str, str]:
"""Get directory structure and files for a package.""" """Get directory structure and files for a package."""
safe_name = name.lower().replace(" ", "_") safe_name = name.lower().replace(" ", "_")
pkg_name = f"pkg_{safe_name}" pkg_name = f"pkg_{safe_name}"
manifest = f"""<?xml version="1.0" encoding="utf-8"?> manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="package" version="4.0" method="upgrade"> <extension type="package" version="4.0" method="upgrade">
<name>{name}</name> <name>{name}</name>
<packagename>{safe_name}</packagename> <packagename>{safe_name}</packagename>
<author>{author}</author> <author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate> <creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright> <copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail> <authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl> <authorUrl>https://example.com</authorUrl>
<version>1.0.0</version> <version>1.0.0</version>
<description>{description}</description> <description>{description}</description>
<files folder="packages"> <files folder="packages">
<!-- Add extension packages here --> <!-- Add extension packages here -->
</files> </files>
</extension> </extension>
""" """
return { return {
f"{pkg_name}.xml": manifest, f"{pkg_name}.xml": manifest,
"packages/.gitkeep": "", "packages/.gitkeep": "",
} }
# ============================================================================ # ============================================================================
@@ -319,59 +318,59 @@ def get_package_structure(name: str, description: str, author: str) -> Dict[str,
# ============================================================================ # ============================================================================
def create_extension( def create_extension(
ext_type: str, ext_type: str,
name: str, name: str,
description: str, description: str,
author: str, author: str,
output_dir: str = "src", output_dir: str = "src",
**kwargs **kwargs
) -> None: ) -> None:
""" """
Create extension scaffolding. Create extension scaffolding.
Args: Args:
ext_type: Extension type (component, module, plugin, template, package) ext_type: Extension type (component, module, plugin, template, package)
name: Extension name name: Extension name
description: Extension description description: Extension description
author: Author name author: Author name
output_dir: Output directory output_dir: Output directory
**kwargs: Additional type-specific options **kwargs: Additional type-specific options
""" """
output_path = Path(output_dir) output_path = Path(output_dir)
# Get structure based on type # Get structure based on type
if ext_type == "component": if ext_type == "component":
structure = get_component_structure(name, description, author) structure = get_component_structure(name, description, author)
elif ext_type == "module": elif ext_type == "module":
client = kwargs.get("client", "site") client = kwargs.get("client", "site")
structure = get_module_structure(name, description, author, client) structure = get_module_structure(name, description, author, client)
elif ext_type == "plugin": elif ext_type == "plugin":
group = kwargs.get("group", "system") group = kwargs.get("group", "system")
structure = get_plugin_structure(name, description, author, group) structure = get_plugin_structure(name, description, author, group)
elif ext_type == "template": elif ext_type == "template":
structure = get_template_structure(name, description, author) structure = get_template_structure(name, description, author)
elif ext_type == "package": elif ext_type == "package":
structure = get_package_structure(name, description, author) structure = get_package_structure(name, description, author)
else: else:
common.die(f"Unknown extension type: {ext_type}") common.die(f"Unknown extension type: {ext_type}")
# Create files # Create files
common.log_section(f"Creating {ext_type}: {name}") common.log_section(f"Creating {ext_type}: {name}")
for file_path, content in structure.items(): for file_path, content in structure.items():
full_path = output_path / file_path full_path = output_path / file_path
# Create parent directories # Create parent directories
full_path.parent.mkdir(parents=True, exist_ok=True) full_path.parent.mkdir(parents=True, exist_ok=True)
# Write file # Write file
full_path.write_text(content, encoding="utf-8") full_path.write_text(content, encoding="utf-8")
common.log_success(f"Created: {file_path}") common.log_success(f"Created: {file_path}")
common.log_section("Scaffolding Complete") common.log_section("Scaffolding Complete")
common.log_info(f"Extension files created in: {output_path}") common.log_info(f"Extension files created in: {output_path}")
common.log_info(f"Extension type: {ext_type}") common.log_info(f"Extension type: {ext_type}")
common.log_info(f"Extension name: {name}") common.log_info(f"Extension name: {name}")
# ============================================================================ # ============================================================================
@@ -379,11 +378,11 @@ def create_extension(
# ============================================================================ # ============================================================================
def main() -> None: def main() -> None:
"""Main entry point.""" """Main entry point."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Create Joomla extension scaffolding", description="Create Joomla extension scaffolding",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples:
# Create a component # Create a component
%(prog)s component MyComponent "My Component Description" "John Doe" %(prog)s component MyComponent "My Component Description" "John Doe"
@@ -400,49 +399,49 @@ Examples:
# Create a package # Create a package
%(prog)s package mypackage "My Package Description" "John Doe" %(prog)s package mypackage "My Package Description" "John Doe"
""" """
) )
parser.add_argument( parser.add_argument(
"type", "type",
choices=["component", "module", "plugin", "template", "package"], choices=["component", "module", "plugin", "template", "package"],
help="Extension type to create" help="Extension type to create"
) )
parser.add_argument("name", help="Extension name") parser.add_argument("name", help="Extension name")
parser.add_argument("description", help="Extension description") parser.add_argument("description", help="Extension description")
parser.add_argument("author", help="Author name") parser.add_argument("author", help="Author name")
parser.add_argument( parser.add_argument(
"-o", "--output", "-o", "--output",
default="src", default="src",
help="Output directory (default: src)" help="Output directory (default: src)"
) )
parser.add_argument( parser.add_argument(
"--client", "--client",
choices=["site", "administrator"], choices=["site", "administrator"],
default="site", default="site",
help="Module client (site or administrator)" help="Module client (site or administrator)"
) )
parser.add_argument( parser.add_argument(
"--group", "--group",
default="system", default="system",
help="Plugin group (system, content, user, etc.)" help="Plugin group (system, content, user, etc.)"
) )
args = parser.parse_args() args = parser.parse_args()
try: try:
create_extension( create_extension(
ext_type=args.type, ext_type=args.type,
name=args.name, name=args.name,
description=args.description, description=args.description,
author=args.author, author=args.author,
output_dir=args.output, output_dir=args.output,
client=args.client, client=args.client,
group=args.group group=args.group
) )
except Exception as e: except Exception as e:
common.log_error(f"Failed to create extension: {e}") common.log_error(f"Failed to create extension: {e}")
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -33,149 +33,149 @@ BRIEF: Run all validation scripts
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import List, Tuple from typing import Tuple
# Add lib directory to path # Add lib directory to path
sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
try: try:
import common import common
except ImportError: except ImportError:
print("ERROR: Cannot import required libraries", file=sys.stderr) print("ERROR: Cannot import required libraries", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Required validation scripts (must pass) # Required validation scripts (must pass)
REQUIRED_SCRIPTS = [ REQUIRED_SCRIPTS = [
"scripts/validate/manifest.py", "scripts/validate/manifest.py",
"scripts/validate/xml_wellformed.py", "scripts/validate/xml_wellformed.py",
"scripts/validate/workflows.py", "scripts/validate/workflows.py",
] ]
# Optional validation scripts (failures are warnings) # Optional validation scripts (failures are warnings)
OPTIONAL_SCRIPTS = [ OPTIONAL_SCRIPTS = [
"scripts/validate/changelog.py", "scripts/validate/changelog.py",
"scripts/validate/language_structure.py", "scripts/validate/language_structure.py",
"scripts/validate/license_headers.py", "scripts/validate/license_headers.py",
"scripts/validate/no_secrets.py", "scripts/validate/no_secrets.py",
"scripts/validate/paths.py", "scripts/validate/paths.py",
"scripts/validate/php_syntax.py", "scripts/validate/php_syntax.py",
"scripts/validate/tabs.py", "scripts/validate/tabs.py",
"scripts/validate/version_alignment.py", "scripts/validate/version_alignment.py",
"scripts/validate/version_hierarchy.py", "scripts/validate/version_hierarchy.py",
] ]
def run_validation_script(script_path: str) -> Tuple[bool, str]: def run_validation_script(script_path: str) -> Tuple[bool, str]:
""" """
Run a validation script. Run a validation script.
Args: Args:
script_path: Path to script script_path: Path to script
Returns: Returns:
Tuple of (success, output) Tuple of (success, output)
""" """
script = Path(script_path) script = Path(script_path)
if not script.exists(): if not script.exists():
return (False, f"Script not found: {script_path}") return (False, f"Script not found: {script_path}")
try: try:
result = subprocess.run( result = subprocess.run(
["python3", str(script)], ["python3", str(script)],
capture_output=True, capture_output=True,
text=True, text=True,
check=False check=False
) )
output = result.stdout + result.stderr output = result.stdout + result.stderr
success = result.returncode == 0 success = result.returncode == 0
return (success, output) return (success, output)
except Exception as e: except Exception as e:
return (False, f"Error running script: {e}") return (False, f"Error running script: {e}")
def main() -> int: def main() -> int:
"""Main entry point.""" """Main entry point."""
common.log_section("Running All Validations") common.log_section("Running All Validations")
print() print()
total_passed = 0 total_passed = 0
total_failed = 0 total_failed = 0
total_skipped = 0 total_skipped = 0
# Run required scripts # Run required scripts
common.log_info("=== Required Validations ===") common.log_info("=== Required Validations ===")
print() print()
for script in REQUIRED_SCRIPTS: for script in REQUIRED_SCRIPTS:
script_name = Path(script).name script_name = Path(script).name
common.log_info(f"Running {script_name}...") common.log_info(f"Running {script_name}...")
success, output = run_validation_script(script) success, output = run_validation_script(script)
if success: if success:
common.log_success(f"{script_name} passed") common.log_success(f"{script_name} passed")
total_passed += 1 total_passed += 1
else: else:
common.log_error(f"{script_name} FAILED") common.log_error(f"{script_name} FAILED")
if output: if output:
print(output) print(output)
total_failed += 1 total_failed += 1
print() print()
# Run optional scripts # Run optional scripts
common.log_info("=== Optional Validations ===") common.log_info("=== Optional Validations ===")
print() print()
for script in OPTIONAL_SCRIPTS: for script in OPTIONAL_SCRIPTS:
script_name = Path(script).name script_name = Path(script).name
if not Path(script).exists(): if not Path(script).exists():
common.log_warn(f"{script_name} not found (skipped)") common.log_warn(f"{script_name} not found (skipped)")
total_skipped += 1 total_skipped += 1
continue continue
common.log_info(f"Running {script_name}...") common.log_info(f"Running {script_name}...")
success, output = run_validation_script(script) success, output = run_validation_script(script)
if success: if success:
common.log_success(f"{script_name} passed") common.log_success(f"{script_name} passed")
total_passed += 1 total_passed += 1
else: else:
common.log_warn(f"{script_name} failed (optional)") common.log_warn(f"{script_name} failed (optional)")
if output: if output:
print(output[:500]) # Limit output print(output[:500]) # Limit output
total_failed += 1 total_failed += 1
print() print()
# Summary # Summary
common.log_section("Validation Summary") common.log_section("Validation Summary")
common.log_kv("Total Passed", str(total_passed)) common.log_kv("Total Passed", str(total_passed))
common.log_kv("Total Failed", str(total_failed)) common.log_kv("Total Failed", str(total_failed))
common.log_kv("Total Skipped", str(total_skipped)) common.log_kv("Total Skipped", str(total_skipped))
print() print()
# Check if any required validations failed # Check if any required validations failed
required_failed = sum( required_failed = sum(
1 for script in REQUIRED_SCRIPTS 1 for script in REQUIRED_SCRIPTS
if Path(script).exists() and not run_validation_script(script)[0] if Path(script).exists() and not run_validation_script(script)[0]
) )
if required_failed > 0: if required_failed > 0:
common.log_error(f"{required_failed} required validation(s) failed") common.log_error(f"{required_failed} required validation(s) failed")
return 1 return 1
common.log_success("All required validations passed!") common.log_success("All required validations passed!")
if total_failed > 0: if total_failed > 0:
common.log_warn(f"{total_failed} optional validation(s) failed") common.log_warn(f"{total_failed} optional validation(s) failed")
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@@ -33,185 +33,184 @@ NOTE: Checks YAML syntax, structure, and best practices
import sys import sys
from pathlib import Path from pathlib import Path
from typing import List, Tuple
# Add lib directory to path # Add lib directory to path
sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
try: try:
import common import common
except ImportError: except ImportError:
print("ERROR: Cannot import required libraries", file=sys.stderr) print("ERROR: Cannot import required libraries", file=sys.stderr)
sys.exit(1) sys.exit(1)
def validate_yaml_syntax(filepath: Path) -> bool: def validate_yaml_syntax(filepath: Path) -> bool:
""" """
Validate YAML syntax of a workflow file. Validate YAML syntax of a workflow file.
Args: Args:
filepath: Path to workflow file filepath: Path to workflow file
Returns: Returns:
True if valid True if valid
""" """
try: try:
import yaml import yaml
except ImportError: except ImportError:
common.log_warn("PyYAML module not installed. Install with: pip3 install pyyaml") common.log_warn("PyYAML module not installed. Install with: pip3 install pyyaml")
return True # Skip validation if yaml not available return True # Skip validation if yaml not available
try: try:
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, 'r', encoding='utf-8') as f:
yaml.safe_load(f) yaml.safe_load(f)
print(f"✓ Valid YAML: {filepath.name}") print(f"✓ Valid YAML: {filepath.name}")
return True return True
except yaml.YAMLError as e: except yaml.YAMLError as e:
print(f"✗ YAML Error in {filepath.name}: {e}", file=sys.stderr) print(f"✗ YAML Error in {filepath.name}: {e}", file=sys.stderr)
return False return False
except Exception as e: except Exception as e:
print(f"✗ Error reading {filepath.name}: {e}", file=sys.stderr) print(f"✗ Error reading {filepath.name}: {e}", file=sys.stderr)
return False return False
def check_no_tabs(filepath: Path) -> bool: def check_no_tabs(filepath: Path) -> bool:
""" """
Check that file contains no tab characters. Check that file contains no tab characters.
Args: Args:
filepath: Path to file filepath: Path to file
Returns: Returns:
True if no tabs found True if no tabs found
""" """
try: try:
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
if '\t' in content: if '\t' in content:
common.log_error(f"✗ File contains tab characters: {filepath.name}") common.log_error(f"✗ File contains tab characters: {filepath.name}")
return False return False
except Exception as e: except Exception as e:
common.log_warn(f"Could not read {filepath}: {e}") common.log_warn(f"Could not read {filepath}: {e}")
return False return False
return True return True
def check_workflow_structure(filepath: Path) -> bool: def check_workflow_structure(filepath: Path) -> bool:
""" """
Check workflow file structure for required keys. Check workflow file structure for required keys.
Args: Args:
filepath: Path to workflow file filepath: Path to workflow file
Returns: Returns:
True if structure is valid True if structure is valid
""" """
errors = 0 errors = 0
try: try:
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Check for required top-level keys # Check for required top-level keys
if 'name:' not in content and not content.startswith('name:'): if 'name:' not in content and not content.startswith('name:'):
common.log_warn(f"Missing 'name:' in {filepath.name}") common.log_warn(f"Missing 'name:' in {filepath.name}")
if 'on:' not in content and not content.startswith('on:'): if 'on:' not in content and not content.startswith('on:'):
common.log_error(f"✗ Missing 'on:' trigger in {filepath.name}") common.log_error(f"✗ Missing 'on:' trigger in {filepath.name}")
errors += 1 errors += 1
if 'jobs:' not in content and not content.startswith('jobs:'): if 'jobs:' not in content and not content.startswith('jobs:'):
common.log_error(f"✗ Missing 'jobs:' in {filepath.name}") common.log_error(f"✗ Missing 'jobs:' in {filepath.name}")
errors += 1 errors += 1
except Exception as e: except Exception as e:
common.log_error(f"Error reading {filepath}: {e}") common.log_error(f"Error reading {filepath}: {e}")
return False return False
return errors == 0 return errors == 0
def validate_workflow_file(filepath: Path) -> bool: def validate_workflow_file(filepath: Path) -> bool:
""" """
Validate a single workflow file. Validate a single workflow file.
Args: Args:
filepath: Path to workflow file filepath: Path to workflow file
Returns: Returns:
True if valid True if valid
""" """
common.log_info(f"Validating: {filepath.name}") common.log_info(f"Validating: {filepath.name}")
errors = 0 errors = 0
# Check YAML syntax # Check YAML syntax
if not validate_yaml_syntax(filepath): if not validate_yaml_syntax(filepath):
errors += 1 errors += 1
# Check for tabs # Check for tabs
if not check_no_tabs(filepath): if not check_no_tabs(filepath):
errors += 1 errors += 1
# Check structure # Check structure
if not check_workflow_structure(filepath): if not check_workflow_structure(filepath):
errors += 1 errors += 1
if errors == 0: if errors == 0:
common.log_info(f"{filepath.name} passed all checks") common.log_info(f"{filepath.name} passed all checks")
return True return True
else: else:
common.log_error(f"{filepath.name} failed {errors} check(s)") common.log_error(f"{filepath.name} failed {errors} check(s)")
return False return False
def main() -> int: def main() -> int:
"""Main entry point.""" """Main entry point."""
common.log_info("GitHub Actions Workflow Validation") common.log_info("GitHub Actions Workflow Validation")
common.log_info("===================================") common.log_info("===================================")
print() print()
workflows_dir = Path(".github/workflows") workflows_dir = Path(".github/workflows")
if not workflows_dir.is_dir(): if not workflows_dir.is_dir():
common.log_error(f"Workflows directory not found: {workflows_dir}") common.log_error(f"Workflows directory not found: {workflows_dir}")
return 1 return 1
# Find all workflow files # Find all workflow files
workflow_files = [] workflow_files = []
for pattern in ["*.yml", "*.yaml"]: for pattern in ["*.yml", "*.yaml"]:
workflow_files.extend(workflows_dir.glob(pattern)) workflow_files.extend(workflows_dir.glob(pattern))
if not workflow_files: if not workflow_files:
common.log_warn("No workflow files found") common.log_warn("No workflow files found")
return 0 return 0
total = len(workflow_files) total = len(workflow_files)
passed = 0 passed = 0
failed = 0 failed = 0
for workflow in workflow_files: for workflow in workflow_files:
if validate_workflow_file(workflow): if validate_workflow_file(workflow):
passed += 1 passed += 1
else: else:
failed += 1 failed += 1
print() print()
common.log_info("===================================") common.log_info("===================================")
common.log_info("Summary:") common.log_info("Summary:")
common.log_info(f" Total workflows: {total}") common.log_info(f" Total workflows: {total}")
common.log_info(f" Passed: {passed}") common.log_info(f" Passed: {passed}")
common.log_info(f" Failed: {failed}") common.log_info(f" Failed: {failed}")
common.log_info("===================================") common.log_info("===================================")
if failed > 0: if failed > 0:
common.log_error("Workflow validation failed") common.log_error("Workflow validation failed")
return 1 return 1
common.log_info("All workflows validated successfully") common.log_info("All workflows validated successfully")
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())