File: D:/web/matomo.vdk/plugins/CoreConsole/Commands/ComputeJsAssetSize.php
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreConsole\Commands;
use Piwik\AssetManager;
use Piwik\AssetManager\UIAssetFetcher\PluginUmdAssetFetcher;
use Piwik\Development;
use Piwik\Metrics\Formatter;
use Piwik\Plugin;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugin\Manager;
use Piwik\ProxyHttp;
use Piwik\SettingsPiwik;
use Piwik\Theme;
/**
* @phpstan-type TotalsType array{merged: int, gzip: int}
*/
class ComputeJsAssetSize extends ConsoleCommand
{
/**
* @var TotalsType
*/
private $totals = [
'merged' => 0,
'gzip' => 0,
];
protected function configure()
{
$this->setName('development:compute-js-asset-size');
$this->setDescription('Generates production assets and computes the size of the resulting code.');
$this->addNoValueOption('no-delete', null, 'Do not delete files after creating them.');
$this->addRequiredValueOption('plugin', null, 'For submodule plugins and 3rd party plugins.');
}
public function isEnabled()
{
return SettingsPiwik::isGitDeployment();
}
protected function doExecute(): int
{
$input = $this->getInput();
$output = $this->getOutput();
$noDelete = $input->getOption('no-delete');
/** @var null|string $plugin */
$plugin = $input->getOption('plugin');
if (!empty($plugin) && !Manager::getInstance()->isValidPluginName($plugin)) {
throw new \Exception('Invalid plugin name: ' . $plugin);
}
$this->checkDevelopmentModeDisabled();
$this->ensureThirdPartyPluginsActivated($plugin);
$output->writeln("Building and printing sizes of built JS assets...");
$fetcher = $this->makeUmdFetcher();
$this->deleteMergedAssets();
$this->buildAssets($fetcher);
$output->writeln('');
$this->printCurrentGitHashAndBranch($plugin);
$output->writeln('');
$this->printFilesizes($fetcher);
if (!$noDelete) {
$this->deleteMergedAssets();
}
return self::SUCCESS;
}
private function ensureThirdPartyPluginsActivated(?string $plugin = null): void
{
$expectedPluginsLoadedAndActivated = [
"CorePluginsAdmin",
"CoreAdminHome",
"CoreHome",
"WebsiteMeasurable",
"IntranetMeasurable",
"Diagnostics",
"CoreVisualizations",
"Proxy",
"API",
"Widgetize",
"Transitions",
"LanguagesManager",
"Actions",
"Dashboard",
"MultiSites",
"Referrers",
"UserLanguage",
"DevicesDetection",
"Goals",
"Ecommerce",
"SEO",
"Events",
"UserCountry",
"GeoIp2",
"VisitsSummary",
"VisitFrequency",
"VisitTime",
"VisitorInterest",
"RssWidget",
"Feedback",
"Monolog",
"Login",
"TwoFactorAuth",
"UsersManager",
"SitesManager",
"Installation",
"CoreUpdater",
"CoreConsole",
"ScheduledReports",
"UserCountryMap",
"Live",
"PrivacyManager",
"ImageGraph",
"Annotations",
"MobileMessaging",
"Overlay",
"SegmentEditor",
"Insights",
"Morpheus",
"Contents",
"TestRunner",
"BulkTracking",
"Resolution",
"DevicePlugins",
"Heartbeat",
"Intl",
"UserId",
"CustomJsTracker",
"Tour",
"PagePerformance",
"CustomDimensions",
"TagManager",
"AbTesting",
"ActivityLog",
"Bandwidth",
"Cohorts",
"CustomAlerts",
"CustomReports",
"CustomVariables",
"DeviceDetectorCache",
"FormAnalytics",
"Funnels",
"GoogleAnalyticsImporter",
"InvalidateReports",
"MarketingCampaignsReporting",
"MediaAnalytics",
"MultiChannelConversionAttribution",
"QueuedTracking",
"RollUpReporting",
"SearchEngineKeywordsPerformance",
"UsersFlow",
"VisitorGenerator",
"WhiteLabel",
"WooCommerceAnalytics",
"AdvertisingConversionExport",
"AnonymousPiwikUsageMeasurement",
];
if ($plugin) {
$expectedPluginsLoadedAndActivated[] = $plugin;
}
if (is_file(PIWIK_INCLUDE_PATH . '/plugins/CoreVue/plugin.json')) {
$expectedPluginsLoadedAndActivated[] = "CoreVue";
}
$expectedPluginsLoadedAndActivated = array_unique($expectedPluginsLoadedAndActivated);
$pluginsLoadedAndActivated = Manager::getInstance()->getPluginsLoadedAndActivated();
$pluginsLoadedAndActivated = array_map(function (Plugin $p) {
return $p->getPluginName();
}, $pluginsLoadedAndActivated);
$missingPlugins = array_diff($expectedPluginsLoadedAndActivated, $pluginsLoadedAndActivated);
if (!empty($missingPlugins)) {
throw new \Exception("Activate the following plugins before running this command: " . implode(", ", $missingPlugins));
}
}
private function buildAssets(PluginUmdAssetFetcher $fetcher): void
{
AssetManager::getInstance()->getMergedCoreJavaScript();
AssetManager::getInstance()->getMergedNonCoreJavaScript();
$chunks = $fetcher->getChunkFiles();
foreach ($chunks as $chunk) {
AssetManager::getInstance()->getMergedJavaScriptChunk($chunk->getChunkName());
}
}
private function deleteMergedAssets(): void
{
AssetManager::getInstance()->removeMergedAssets();
}
private function printFilesizes(PluginUmdAssetFetcher $fetcher): void
{
$fileSizes = [];
$mergedCore = AssetManager::getInstance()->getMergedCoreJavaScript();
$fileSizes[] = $this->getFileSizeRow($mergedCore);
$mergedNonCore = AssetManager::getInstance()->getMergedNonCoreJavaScript();
$fileSizes[] = $this->getFileSizeRow($mergedNonCore);
$chunks = $fetcher->getChunkFiles();
foreach ($chunks as $chunk) {
$chunkAsset = AssetManager::getInstance()->getMergedJavaScriptChunk($chunk->getChunkName());
$fileSizes[] = $this->getFileSizeRow($chunkAsset);
}
$fileSizes[] = [];
$fileSizes[] = ['Total', $this->getFormattedSize($this->totals['merged']), $this->getFormattedSize($this->totals['gzip'])];
$this->renderTable(['File', 'Size', 'Size (gzipped)'], $fileSizes);
}
/**
* @param key-of<TotalsType> $type
*/
private function getFileSize(string $fileLocation, string $type): string
{
$size = (int)filesize($fileLocation);
$this->totals[$type] += $size;
return $this->getFormattedSize($size);
}
private function getFormattedSize(int $size): string
{
$formatter = new Formatter();
return $formatter->getPrettySizeFromBytes($size, 'K', 2);
}
private function checkDevelopmentModeDisabled(): void
{
if (Development::isEnabled()) {
throw new \Exception("This command is to estimate production build sizes, so development mode must be disabled for it.");
}
}
private function getGzippedFileSize(string $path): string
{
$data = file_get_contents($path);
$data = ProxyHttp::gzencode($data);
if (false === $data) {
throw new \Exception('compressing file ' . $path . ' failed');
}
$compressedPath = dirname($path) . '/' . basename($path) . '.gz';
file_put_contents($compressedPath, $data);
return $this->getFileSize($compressedPath, 'gzip');
}
private function printCurrentGitHashAndBranch(?string $plugin = null): void
{
$branchName = trim(shell_exec("git rev-parse --abbrev-ref HEAD") ?: '');
$lastCommit = trim(shell_exec("git log --pretty=format:'%h' -n 1") ?: '');
$pluginSuffix = '';
if ($plugin) {
$prefix = 'cd ' . escapeshellarg(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin) . '; ';
$pluginBranchName = trim(shell_exec($prefix . 'git rev-parse --abbrev-ref HEAD') ?: '');
$pluginLastCommit = trim(shell_exec($prefix . 'git log --pretty=format:\'%h\' -n 1') ?: '');
$pluginSuffix = " [$plugin: $pluginBranchName ($pluginLastCommit)]";
}
$this->getOutput()->writeln("<info>$branchName ($lastCommit)$pluginSuffix</info>");
}
private function makeUmdFetcher(): PluginUmdAssetFetcher
{
$plugins = Manager::getInstance()->getPluginsLoadedAndActivated();
$pluginNames = array_map(function ($p) {
return $p->getPluginName();
}, $plugins);
$theme = Manager::getInstance()->getThemeEnabled();
if (!empty($theme)) {
$theme = new Theme();
}
return new PluginUmdAssetFetcher($pluginNames, $theme, null);
}
/**
* @return array{0: string, 1: string, 2: string}
*/
private function getFileSizeRow(AssetManager\UIAsset $asset): array
{
return [$asset->getRelativeLocation(), $this->getFileSize($asset->getAbsoluteLocation(), 'merged'), $this->getGzippedFileSize($asset->getAbsoluteLocation())];
}
}