Skip to content

Instantly share code, notes, and snippets.

@datvance
Created January 20, 2022 15:56
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save datvance/09dfb274c77e2a8104e415f8f1854557 to your computer and use it in GitHub Desktop.
Save datvance/09dfb274c77e2a8104e415f8f1854557 to your computer and use it in GitHub Desktop.
A composer post-update/post-install command to find, download, copy, whatever front-end library files specified in a drupal module's "composer.libraries.json" file (which is non-standard).
<?php
/**
* A hack.
*
* A composer post-update/post-install command to find, download, copy, whatever front-end library files
* specified in a drupal module's "composer.libraries.json" file (which is non-standard). This is a replacement
* for the wikimedia/composer-merge-plugin package which is deprecated and causes issues on Pantheon
* See: https://www.drupal.org/project/webform/issues/3088336#comment-13312823
* And: https://www.drupal.org/project/documentation/issues/2605130
*
*
* - find and parse composer.libraries.json files
* - for each package found:
* - download dist file (zip or tar) to tmp dir
* - uncompress dist file
* - move dist file to library directory
* - clean up
*/
class LibraryInstaller
{
protected array $libraries = [];
protected string $libraries_path = '';
protected string $modules_path = '';
protected string $tmp_dir = '';
protected bool $debug = true;
public function __construct()
{
$drupal_root = realpath(dirname(dirname(__DIR__)) . '/web');
//Pantheon places all modules managed by composer here
$this->modules_path = $drupal_root . '/modules/composer';
$this->libraries_path = $drupal_root . '/libraries';
//hope this is writable
$this->tmp_dir = sys_get_temp_dir() . '/' . time();
}
/**
* @return void
*/
public function run()
{
foreach($this->findLibraryFiles() as $file)
{
$libraries = $this->parseLibraryFile($file);
if($libraries)
{
$this->mergeLibraries($libraries);
}
}
if(!$this->libraries)
{
echo "No libraries found\n";
exit;
}
$this->prepareLibraryDirectory();
$this->prepareTmpDirectory();
foreach($this->libraries as $library)
{
$this->processLibrary($library);
}
$this->cleanUp();
}
/**
* @return array
*/
protected function findLibraryFiles(): array
{
$library_files = glob($this->modules_path . '/*/composer.libraries.json');
return $library_files === false ? [] : $library_files;
}
/**
* @param $file
* @return array
*/
protected function parseLibraryFile($file): array
{
$json = json_decode(file_get_contents($file));
if(!isset($json->repositories)) return [];
$libraries = [];
foreach($json->repositories as $repo)
{
if(isset($repo->package->type) && $repo->package->type == 'drupal-library')
{
$libraries[] = $repo->package;
}
}
return $libraries;
}
/**
* Different modules could specify the same library, possibly different versions.
* What's the correct thing to do? What does composer do?
* We'll just choose the latest version and hope.
*
* @param $libraries
* @return void
*/
protected function mergeLibraries($libraries)
{
foreach($libraries as $library)
{
$name = $library->extra->{'installer-name'};
if(isset($this->libraries[$name]))
{
$v1 = str_replace('v', '', $library->version);
$v2 = str_replace('v', '', $this->libraries[$name]->version);
if(version_compare($v1, $v2, 'gt'))
{
$this->libraries[$name] = $library;
}
}
else
{
$this->libraries[$name] = $library;
}
}
}
/**
* @param stdClass $library
* @return false|void
*/
protected function processLibrary(stdClass $library)
{
$compressed_library = $this->downloadLibrary($library);
if(!$compressed_library) return false;
$uncompressed_library = $this->uncompressLibrary($library, $compressed_library);
if(!$uncompressed_library) return false;
$this->placeLibrary($library, $uncompressed_library);
}
/**
* @param $library
* @return false|string
*/
protected function downloadLibrary($library)
{
$where = $this->tmp_dir . '/' . $library->extra->{'installer-name'};
if(!mkdir($where, 0777, true)) return false;
//the compressed file name
$file_name = basename($library->dist->url);
//the path to the downloaded compressed file
$dist_file = "$where/$file_name";
if($this->debug) echo "\ndownloading to $dist_file\n";
`curl --location --output {$dist_file} {$library->dist->url}`;
return file_exists($dist_file) ? $dist_file : false;
}
/**
* @param stdClass $library
* @param string $compressed_library
* @return false|string
*/
protected function uncompressLibrary(stdClass $library, string $compressed_library)
{
switch($library->dist->type)
{
case 'tar':
$command = 'tar -xzf';
break;
case 'zip':
$command = 'unzip -o';
break;
case 'file':
return $compressed_library;
break;
default:
$command = '';
}
if(!$command)
{
if($this->debug) echo "Not a compressed dist library.\n";
return false;
}
$location = dirname($compressed_library);
$compressed_file = basename($compressed_library);
`cd {$location} && {$command} {$compressed_file} && rm {$compressed_library}`;
//try to figure out what the dist got expanded to
//if there is a common file in the top directory, that's the library
if(file_exists("{$location}/package.json") ||
file_exists("{$location}/composer.json") ||
file_exists("{$location}/LICENSE.md") ||
file_exists("{$location}/README.md"))
{
$library_dir = $location;
}
else
{
//maybe it got expanded into a sub-directory
$library_dir = $location . '/' . trim(strtok(trim(`ls -AUtm $location`), ','));
}
if($this->debug) echo "Library was expanded to {$library_dir}.\n";
return $library_dir && is_dir($library_dir) ? $library_dir : false;
}
/**
* @param stdClass $library
* @param $expanded_library
* @return void
*/
protected function placeLibrary(stdClass $library, $expanded_library)
{
$library_name = $library->dist->url == 'file'
? basename($library->dist->url)
: $library->extra->{'installer-name'};
//its versioned, something like "tippyjs/5.x"
if(strpos($library->extra->{'installer-name'}, '/') !== false)
{
$path = $library->extra->{'installer-name'};
mkdir("{$this->libraries_path}/{$path}", 0755, true);
}
if($this->debug) echo "Moving {$expanded_library} to {$this->libraries_path}/{$library_name}.\n";
`mv {$expanded_library} {$this->libraries_path}/{$library_name}`;
}
protected function cleanUp()
{
@rmdir($this->tmp_dir);
}
/**
* @return bool
*/
protected function prepareLibraryDirectory(): bool
{
if(is_dir($this->libraries_path) && is_writable($this->libraries_path))
{
return true;
}
return mkdir($this->libraries_path, 0755);
}
/**
* @return bool
*/
protected function prepareTmpDirectory(): bool
{
if(is_dir($this->tmp_dir) && is_writable($this->tmp_dir))
{
return true;
}
return mkdir($this->tmp_dir);
}
}
(new LibraryInstaller())->run();
@datvance
Copy link
Author

You’ll probably have to change line 31 depending on where you put the script and where your webroot is, but other than that it “should” work on Pantheon D9 integrated composer.

@datvance
Copy link
Author

datvance commented Feb 1, 2022

Add the script as a post-install/post-update command in your composer.json, pointing to wherever your script actually lives
"scripts": { "post-install-cmd": [ "php ./assets/scripts/install-libraries.php" ], "post-update-cmd": [ "php ./assets/scripts/install-libraries.php" ] }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment