chore: anchor Composer vendor ignore to /vendor/ (allow nested vendored assets) #24

Closed
jmiller wants to merge 10 commits from git/gitignore-vendor-anchor into main
12 changed files with 278 additions and 10 deletions
+15 -1
View File
@@ -46,6 +46,7 @@ Icon?
.idea/
.settings/
.claude/
.gemini/
.vscode/*
!.vscode/tasks.json
!.vscode/settings.json.example
@@ -153,7 +154,7 @@ package-lock.json
# ============================================================
# PHP / Composer tooling
# ============================================================
vendor/
/vendor/
!src/media/vendor/
composer.lock
*.phar
@@ -204,3 +205,16 @@ hypothesis/
profile.ps1
.mcp.json
# ============================================================
# AI client instructions (not version controlled)
# ============================================================
/GEMINI.md
/MOKOAI.md
.mokoai/
# ============================================================
# Local wiki clone (not version controlled)
# ============================================================
wiki/
docs/
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Automation
# VERSION: 01.00.00
# VERSION: 01.00.40
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -39,7 +39,7 @@ Joomla **package** (`pkg_mokosuitesight`) — standalone module, auto-discovers
## Rules
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Never commit** `.claude/`, `.gemini/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/`
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
VERSION: 01.00.29
VERSION: 01.00.40
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Community expectations and enforcement guidelines
NOTE: Adapted with attribution from the Contributor Covenant v2.1
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.Template-Joomla
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
VERSION: 01.00.29
VERSION: 01.00.40
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 01.00.29
VERSION: 01.00.40
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -0,0 +1,179 @@
<?php
/**
* @package MokoSuiteSight
* @subpackage com_mokosuitesight
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GPL-3.0-or-later
*/
declare(strict_types=1);
namespace Moko\Component\MokoSuiteSight\Administrator\Installer;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\Database\DatabaseInterface;
\defined('_JEXEC') or die;
/**
* Self-contained MokoSuite admin-menu registry helper.
*
* Each MokoSuite extension calls register()/unregister() from its install script
* to publish its admin menu items into `#__mokosuite_menu`, which the
* mod_mokosuiteclient_menu module reads and renders. The helper is deliberately
* dependency-free and self-healing (it creates the table if missing) so an
* extension can install BEFORE the Client hub that owns the table.
*
* This file is the canonical contract: copy it verbatim into each MokoSuite
* extension, changing only the namespace to match that extension. Do NOT add
* cross-extension dependencies here.
*
* @since 02.59.07
*/
final class MokoMenuRegistry
{
/**
* Idempotently (re)register one extension's admin menu items.
*
* Each $items entry is an associative array:
* - title (string, required) language key or literal title
* - link (string) index.php?option=... route ('' for a bare group)
* - icon (string) a VERIFIED icon-* glyph (Atum renders many blank)
* - parent (string) owner element of the group this nests under;
* '' means this row IS a top-level group
* - element (string) component this item routes into (for active-state)
* - access (int) Joomla view access level (default 1)
* - ordering (int) sort within its level (default 0)
* - menu_group (string) reserved sidebar section (default 'suite')
* - params (string|null) reserved JSON
*
* @param string $owner Registering extension element, e.g. 'com_mokosuitehq'.
* @param array $items The menu rows to publish for this owner.
*
* @return void
*/
public static function register(string $owner, array $items): void
{
try {
$db = Factory::getContainer()->get(DatabaseInterface::class);
if (!self::ensureTable($db)) {
return;
}
// Delete-then-insert on owner = fully idempotent across reinstall/upgrade.
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokosuite_menu'))
->where($db->quoteName('owner') . ' = ' . $db->quote($owner))
)->execute();
foreach ($items as $item) {
$row = (object) [
'owner' => $owner,
'element' => (string) ($item['element'] ?? ''),
'parent' => (string) ($item['parent'] ?? ''),
'title' => (string) ($item['title'] ?? ''),
'link' => (string) ($item['link'] ?? ''),
'icon' => (string) ($item['icon'] ?? 'icon-puzzle-piece'),
'menu_group' => (string) ($item['menu_group'] ?? 'suite'),
'access' => (int) ($item['access'] ?? 1),
'ordering' => (int) ($item['ordering'] ?? 0),
'published' => (int) ($item['published'] ?? 1),
'params' => $item['params'] ?? null,
];
$db->insertObject('#__mokosuite_menu', $row);
}
} catch (\Throwable $e) {
// Best-effort: a registry failure must never block the install.
Log::add(
'MokoMenuRegistry::register(' . $owner . ') failed: ' . $e->getMessage(),
Log::ERROR,
'com_mokosuitesight'
);
}
}
/**
* Remove all admin menu rows owned by an extension. Call on uninstall.
*
* @param string $owner The registering extension element.
*
* @return void
*/
public static function unregister(string $owner): void
{
try {
$db = Factory::getContainer()->get(DatabaseInterface::class);
if (!self::tableExists($db)) {
return;
}
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokosuite_menu'))
->where($db->quoteName('owner') . ' = ' . $db->quote($owner))
)->execute();
} catch (\Throwable $e) {
Log::add(
'MokoMenuRegistry::unregister(' . $owner . ') failed: ' . $e->getMessage(),
Log::ERROR,
'com_mokosuitesight'
);
}
}
/**
* Create the registry table if it does not exist yet. This lets a sibling
* extension install before the Client hub that ships the table in its schema.
*
* @param DatabaseInterface $db The database driver.
*
* @return boolean True if the table exists (or was created).
*/
private static function ensureTable(DatabaseInterface $db): bool
{
if (self::tableExists($db)) {
return true;
}
$db->setQuery(
'CREATE TABLE IF NOT EXISTS ' . $db->quoteName('#__mokosuite_menu') . ' ('
. '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,'
. "`owner` VARCHAR(75) NOT NULL DEFAULT '',"
. "`element` VARCHAR(75) NOT NULL DEFAULT '',"
. "`parent` VARCHAR(75) NOT NULL DEFAULT '',"
. "`title` VARCHAR(255) NOT NULL DEFAULT '',"
. "`link` VARCHAR(1024) NOT NULL DEFAULT '',"
. "`icon` VARCHAR(75) NOT NULL DEFAULT 'icon-puzzle-piece',"
. "`menu_group` VARCHAR(50) NOT NULL DEFAULT 'suite',"
. '`access` INT UNSIGNED NOT NULL DEFAULT 1,'
. '`ordering` INT NOT NULL DEFAULT 0,'
. '`published` TINYINT(1) NOT NULL DEFAULT 1,'
. '`params` TEXT NULL,'
. 'PRIMARY KEY (`id`),'
. 'UNIQUE KEY `idx_owner_link` (`owner`, `link`(191)),'
. 'KEY `idx_owner` (`owner`),'
. 'KEY `idx_parent` (`parent`),'
. 'KEY `idx_published_ordering` (`published`, `ordering`)'
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
)->execute();
return self::tableExists($db);
}
/**
* Whether the #__mokosuite_menu table currently exists.
*
* @param DatabaseInterface $db The database driver.
*
* @return boolean
*/
private static function tableExists(DatabaseInterface $db): bool
{
return \in_array($db->replacePrefix('#__mokosuite_menu'), $db->getTableList(), true);
}
}
@@ -5,8 +5,9 @@
<creationDate>2026-06-23</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting.</copyright>
<license>GPL-3.0-or-later</license>
<version>01.00.29</version>
<version>01.00.40</version>
<namespace path="src">Moko\Component\MokoSuiteSight</namespace>
<scriptfile>script.php</scriptfile>
<administration>
<files folder="admin"><folder>services</folder><folder>src</folder><folder>tmpl</folder></files>
<menu>COM_MOKOSUITESIGHT</menu>
@@ -0,0 +1,74 @@
<?php
/**
* @package MOKOSUITESIGHT
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Installer\InstallerAdapter;
class Com_mokosuitesightInstallerScript
{
public function postflight(string $type, InstallerAdapter $adapter): void
{
if ($type === 'install' || $type === 'update' || $type === 'discover_install')
{
$this->registerAdminMenu();
}
}
public function uninstall(InstallerAdapter $adapter): void
{
$this->unregisterAdminMenu();
}
private const MENU_OWNER = 'com_mokosuitesight';
private function registerAdminMenu(): void
{
$registry = $this->loadMenuRegistry();
if ($registry !== null)
{
$registry::register(self::MENU_OWNER, $this->getMenuItems());
}
}
private function unregisterAdminMenu(): void
{
$registry = $this->loadMenuRegistry();
if ($registry !== null)
{
$registry::unregister(self::MENU_OWNER);
}
}
private function loadMenuRegistry(): ?string
{
$class = '\\Moko\\Component\\MokoSuiteSight\\Administrator\\Installer\\MokoMenuRegistry';
if (!class_exists($class))
{
$path = JPATH_ADMINISTRATOR . '/components/com_mokosuitesight/src/Installer/MokoMenuRegistry.php';
if (is_file($path))
{
require_once $path;
}
}
return class_exists($class) ? $class : null;
}
private function getMenuItems(): array
{
$owner = self::MENU_OWNER;
return [
['element' => $owner, 'parent' => '', 'title' => 'COM_MOKOSUITESIGHT_SHORT', 'link' => 'index.php?option=com_mokosuitesight', 'icon' => 'icon-eye', 'ordering' => 91],
];
}
}
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>01.00.29</version>
<version>01.00.40</version>
<php_minimum>8.3</php_minimum>
<description>PLG_SYSTEM_MOKOSUITESIGHT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteSight</namespace>
@@ -3,7 +3,7 @@
<name>Web Services - MokoSuite Insight</name>
<element>mokosuitesight</element>
<author>Moko Consulting</author>
<version>01.00.29</version>
<version>01.00.40</version>
<license>GPL-3.0-or-later</license>
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteInsight</namespace>
<files><folder>src</folder><folder>services</folder></files>
+1 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuite Insight</name>
<packagename>mokosuitesight</packagename>
<version>01.00.29</version>
<version>01.00.40</version>
<creationDate>2026-06-21</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>