diff --git a/composer.json b/composer.json
index 42396ea..9544761 100644
--- a/composer.json
+++ b/composer.json
@@ -18,7 +18,7 @@
"extra": {
"class": "SilverStripe\\VendorPlugin\\VendorPlugin",
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.3.x-dev"
}
},
"scripts": {
@@ -27,6 +27,7 @@
},
"minimum-stability": "dev",
"require": {
+ "composer/installers": "^1.4",
"composer-plugin-api": "^1.1"
},
"require-dev": {
diff --git a/src/Console/VendorExposeCommand.php b/src/Console/VendorExposeCommand.php
index dc22a45..4630633 100644
--- a/src/Console/VendorExposeCommand.php
+++ b/src/Console/VendorExposeCommand.php
@@ -6,9 +6,10 @@
use Composer\Factory;
use Composer\IO\ConsoleIO;
use Composer\Util\Filesystem;
+use Generator;
+use SilverStripe\VendorPlugin\Library;
use SilverStripe\VendorPlugin\Util;
use SilverStripe\VendorPlugin\VendorExposeTask;
-use SilverStripe\VendorPlugin\VendorModule;
use SilverStripe\VendorPlugin\VendorPlugin;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -22,7 +23,7 @@ class VendorExposeCommand extends BaseCommand
public function configure()
{
$this->setName('vendor-expose');
- $this->setDescription('Refresh all exposed vendor module folders');
+ $this->setDescription('Refresh all exposed module/theme/project folders');
$this->addArgument(
'method',
InputArgument::OPTIONAL,
@@ -35,16 +36,19 @@ public function execute(InputInterface $input, OutputInterface $output)
{
$io = new ConsoleIO($input, $output, $this->getHelperSet());
- // Check modules to expose
- $modules = $this->getAllModules();
+ // Check libraries to expose
+ $modules = $this->getAllLibraries();
if (empty($modules)) {
$io->write("No modules to expose");
return;
}
+ // Query first library for base destination
+ $basePublicPath = $modules[0]->getBasePublicPath();
+
// Expose all modules
$method = $input->getArgument('method');
- $task = new VendorExposeTask($this->getProjectPath(), new Filesystem(), VendorModule::DEFAULT_TARGET);
+ $task = new VendorExposeTask($this->getProjectPath(), new Filesystem(), $basePublicPath);
$task->process($io, $modules, $method);
// Success
@@ -52,33 +56,98 @@ public function execute(InputInterface $input, OutputInterface $output)
}
/**
- * Find all modules
+ * Get all libraries
*
- * @return VendorModule[]
+ * @return Library[]
*/
- protected function getAllModules()
+ protected function getAllLibraries()
{
$modules = [];
$basePath = $this->getProjectPath();
- $search = Util::joinPaths($basePath, 'vendor', '*', '*');
- foreach (glob($search, GLOB_ONLYDIR) as $modulePath) {
+
+ // Get all modules
+ foreach ($this->getModulePaths() as $modulePath) {
// Filter by non-composer folders
$composerPath = Util::joinPaths($modulePath, 'composer.json');
if (!file_exists($composerPath)) {
continue;
}
- // Build module
- $name = basename($modulePath);
- $vendor = basename(dirname($modulePath));
- $module = new VendorModule($basePath, "{$vendor}/{$name}");
- // Check if this module has folders to expose
- if ($module->getExposedFolders()) {
- $modules[] = $module;
+
+ // Ensure this library should be exposed, and has at least one folder
+ $module = new Library($basePath, $modulePath);
+ if (!$module->requiresExpose() || !$module->getExposedFolders()) {
+ continue;
}
+
+ // Save this module
+ $modules[] = $module;
}
return $modules;
}
+ /**
+ * Find all modules
+ *
+ * @deprecated 1.3..2.0
+ * @return Library[]
+ */
+ protected function getAllModules()
+ {
+ return $this->getAllLibraries();
+ }
+
+ /**
+ * Search all paths that could contain a module / theme
+ *
+ * @return Generator
+ */
+ protected function getModulePaths()
+ {
+ // Project root is always returned
+ $basePath = $this->getProjectPath();
+ yield $basePath;
+
+ // Get vendor modules
+ $search = Util::joinPaths($basePath, 'vendor', '*', '*');
+ foreach (glob($search, GLOB_ONLYDIR) as $modulePath) {
+ if ($this->isPathModule($modulePath)) {
+ yield $modulePath;
+ }
+ }
+
+ // Check if public/ folder exists
+ $publicExists = is_dir(Util::joinPaths($basePath, Library::PUBLIC_PATH));
+ if (!$publicExists) {
+ return;
+ }
+
+ // Search all base folders / modules
+ $search = Util::joinPaths($basePath, '*');
+ foreach (glob($search, GLOB_ONLYDIR) as $modulePath) {
+ if ($this->isPathModule($modulePath)) {
+ yield $modulePath;
+ }
+ }
+
+ // Check all themes
+ $search = Util::joinPaths($basePath, 'themes', '*');
+ foreach (glob($search, GLOB_ONLYDIR) as $themePath) {
+ yield $themePath;
+ }
+ }
+
+ /**
+ * Check if the given path is a silverstripe module
+ *
+ * @param string $path
+ * @return bool
+ */
+ protected function isPathModule($path)
+ {
+ return file_exists(Util::joinPaths($path, '_config'))
+ || file_exists(Util::joinPaths($path, '_config.php'));
+ }
+
/**
* @return string
*/
diff --git a/src/Library.php b/src/Library.php
new file mode 100644
index 0000000..14bd23c
--- /dev/null
+++ b/src/Library.php
@@ -0,0 +1,288 @@
+basePath = $basePath;
+ $this->path = $libraryPath;
+ $this->name = $name;
+ }
+
+ /**
+ * Module name
+ *
+ * @var string
+ */
+ protected $name = null;
+
+ /**
+ * Get module name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ if ($this->name) {
+ return $this->name;
+ }
+ // Get from composer
+ $json = $this->getJson();
+ if (isset($json['name'])) {
+ $this->name = $json['name'];
+ }
+ return $this->name;
+ }
+
+ /**
+ * Get type of library
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ // Get from composer
+ $json = $this->getJson();
+ if (isset($json['type'])) {
+ return $json['type'];
+ }
+ return 'module';
+ }
+
+ /**
+ * Get path to base project for this module
+ *
+ * @return string Path with no trailing slash E.g. /var/www/
+ */
+ public function getBasePath()
+ {
+ return $this->basePath;
+ }
+
+ /**
+ * Get base path to expose all libraries to
+ *
+ * @return string Path with no trailing slash E.g. /var/www/public/resources
+ */
+ public function getBasePublicPath()
+ {
+ $projectPath = $this->getBasePath();
+ $publicPath = $this->publicPathExists()
+ ? Util::joinPaths($projectPath, self::PUBLIC_PATH, self::RESOURCES_PATH)
+ : Util::joinPaths($projectPath, self::RESOURCES_PATH);
+ return $publicPath;
+ }
+
+ /**
+ * Get path for this module
+ *
+ * @return string Path with no trailing slash E.g. /var/www/vendor/silverstripe/module
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Get path relative to base dir.
+ * If module path is base this will be empty string
+ *
+ * @return string Path with trimmed slashes. E.g. vendor/silverstripe/module.
+ * This will be empty for the base project.
+ */
+ public function getRelativePath()
+ {
+ return trim(substr($this->path, strlen($this->basePath)), self::TRIM_CHARS);
+ }
+
+ /**
+ * Get base path to map resources for this module
+ *
+ * @return string Path with trimmed slashes. E.g. /var/www/public/resources/vendor/silverstripe/module
+ */
+ public function getPublicPath()
+ {
+ $relativePath = $this->getRelativePath();
+
+ // 4.0 compatibility: If there is no public folder, and this is a vendor path,
+ // remove the leading `vendor` from the destination
+ if (!$this->publicPathExists() && $this->installedIntoVendor()) {
+ $relativePath = substr($relativePath, strlen('vendor/'));
+ }
+
+ return Util::joinPaths($this->getBasePublicPath(), $relativePath);
+ }
+
+ /**
+ * Cache of composer.json content
+ *
+ * @var array
+ */
+ protected $json = [];
+
+ /**
+ * Get json content for this module from composer.json
+ *
+ * @return array
+ */
+ protected function getJson()
+ {
+ if ($this->json) {
+ return $this->json;
+ }
+ $composer = Util::joinPaths($this->getPath(), 'composer.json');
+ $file = new JsonFile($composer);
+ $this->json = $file->read();
+ return $this->json;
+ }
+
+ /**
+ * Determine if this module should be exposed.
+ * Note: If not using public folders, only vendor modules need to be exposed
+ *
+ * @return bool
+ */
+ public function requiresExpose()
+ {
+ // Don't expose if no folders configured
+ if (!$this->getExposedFolders()) {
+ return false;
+ }
+
+ // Expose if either public root exists, or vendor module
+ return $this->publicPathExists() || $this->installedIntoVendor();
+ }
+
+ /**
+ * Expose all web accessible paths for this module
+ *
+ * @param ExposeMethod $method
+ */
+ public function exposePaths(ExposeMethod $method)
+ {
+ // No-op if exposure not necessary for this configuration
+ if (!$this->requiresExpose()) {
+ return;
+ }
+ $folders = $this->getExposedFolders();
+ $sourcePath = $this->getPath();
+ $targetPath = $this->getPublicPath();
+ foreach ($folders as $folder) {
+ // Get paths for this folder and delegate to expose method
+ $folderSourcePath = Util::joinPaths($sourcePath, $folder);
+ $folderTargetPath = Util::joinPaths($targetPath, $folder);
+ $method->exposeDirectory($folderSourcePath, $folderTargetPath);
+ }
+ }
+
+ /**
+ * Get name of all folders to expose (relative to module root)
+ *
+ * @return array
+ */
+ public function getExposedFolders()
+ {
+ $data = $this->getJson();
+
+ // Only expose if correct type
+ if (empty($data['type']) || !preg_match(VendorPlugin::MODULE_FILTER, $data['type'])) {
+ return [];
+ }
+
+ // Get all dirs to expose
+ if (empty($data['extra']['expose'])) {
+ return [];
+ }
+ $expose = $data['extra']['expose'];
+
+ // Validate all paths are safe
+ foreach ($expose as $exposeFolder) {
+ if (!$this->validateFolder($exposeFolder)) {
+ throw new LogicException("Invalid module folder " . $exposeFolder);
+ }
+ }
+ return $expose;
+ }
+
+ /**
+ * Validate the given folder is allowed
+ *
+ * @param string $exposeFolder Relative folder name to check
+ * @return bool
+ */
+ protected function validateFolder($exposeFolder)
+ {
+ if (strstr($exposeFolder, '.')) {
+ return false;
+ }
+ if (strpos($exposeFolder, '/') === 0) {
+ return false;
+ }
+ if (strpos($exposeFolder, '\\') === 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determin eif the public folder exists
+ *
+ * @return bool
+ */
+ public function publicPathExists()
+ {
+ return is_dir(Util::joinPaths($this->getBasePath(), self::PUBLIC_PATH));
+ }
+
+ /**
+ * Check if this module is installed in vendor
+ *
+ * @return bool
+ */
+ protected function installedIntoVendor()
+ {
+ return preg_match('#^vendor[/\\\\]#', $this->getRelativePath());
+ }
+}
diff --git a/src/Util.php b/src/Util.php
index 28de238..6f47e8a 100644
--- a/src/Util.php
+++ b/src/Util.php
@@ -13,6 +13,7 @@ class Util
public static function joinPaths(...$parts)
{
$combined = null;
+ $parts = array_filter($parts);
array_walk_recursive($parts, function ($part) use (&$combined) {
// Normalise path
$part = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $part);
diff --git a/src/VendorExposeTask.php b/src/VendorExposeTask.php
index 5683056..8f88144 100644
--- a/src/VendorExposeTask.php
+++ b/src/VendorExposeTask.php
@@ -32,37 +32,37 @@ class VendorExposeTask
protected $filesystem;
/**
- * Name of resources folder
+ * Path to expose to
*
* @var string
*/
- protected $resourcesFolder;
+ protected $resourcesPath;
/**
* Construct task for the given base folder
*
* @param string $basePath
* @param Filesystem $filesystem
- * @param string $resourcesFolder Base name of 'resources' folder
+ * @param string $publicPath Public path to expose to
*/
- public function __construct($basePath, Filesystem $filesystem, $resourcesFolder)
+ public function __construct($basePath, Filesystem $filesystem, $publicPath)
{
$this->basePath = $basePath;
$this->filesystem = $filesystem;
- $this->resourcesFolder = $resourcesFolder;
+ $this->resourcesPath = $publicPath;
}
/**
* Expose all modules with the given method
*
* @param IOInterface $io
- * @param VendorModule[] $modules
+ * @param Library[] $libraries
* @param string $methodKey Method key, or null to auto-detect from environment
*/
- public function process(IOInterface $io, array $modules, $methodKey = null)
+ public function process(IOInterface $io, array $libraries, $methodKey = null)
{
// No-op
- if (empty($modules)) {
+ if (empty($libraries)) {
return;
}
@@ -76,10 +76,15 @@ public function process(IOInterface $io, array $modules, $methodKey = null)
$method = $this->getMethod($methodKey);
// Update all modules
- foreach ($modules as $module) {
+ foreach ($libraries as $module) {
+ // Skip this module if no exposure required
+ if (!$module->requiresExpose()) {
+ continue;
+ }
$name = $module->getName();
+ $type = $module->getType();
$io->write(
- "Exposing web directories for module {$name} with method {$methodKey}:"
+ "Exposing web directories for {$type} {$name} with method {$methodKey}:"
);
foreach ($module->getExposedFolders() as $folder) {
$io->write(" - $folder");
@@ -149,7 +154,6 @@ protected function getMethod($key)
}
}
-
/**
* Get 'key' of method to use
*
@@ -204,9 +208,6 @@ protected function getMethodFilePath()
*/
protected function getResourcesPath()
{
- return Util::joinPaths(
- $this->basePath,
- $this->resourcesFolder
- );
+ return $this->resourcesPath;
}
}
diff --git a/src/VendorModule.php b/src/VendorModule.php
index bca85a6..6e4b2e6 100644
--- a/src/VendorModule.php
+++ b/src/VendorModule.php
@@ -2,38 +2,22 @@
namespace SilverStripe\VendorPlugin;
-use Composer\Json\JsonFile;
-use LogicException;
-use SilverStripe\VendorPlugin\Methods\ExposeMethod;
-
/**
* Represents a module in the vendor folder
+ *
+ * @deprecated 1.3..2.0 Use Library instead
*/
-class VendorModule
+class VendorModule extends Library
{
- /**
- * Default replacement folder for 'vendor'
- */
- const DEFAULT_TARGET = 'resources';
-
/**
* Default source folder
*/
const DEFAULT_SOURCE = 'vendor';
/**
- * Project root
- *
- * @var string
- */
- protected $basePath = null;
-
- /**
- * Module name
- *
- * @var string
+ * Default replacement folder for 'vendor'
*/
- protected $name = null;
+ const DEFAULT_TARGET = 'resources';
/**
* Build a vendor module library
@@ -43,112 +27,27 @@ class VendorModule
*/
public function __construct($basePath, $name)
{
- $this->basePath = $basePath;
- $this->name = $name;
- }
-
- /**
- * Get module name
- *
- * @return string
- */
- public function getName()
- {
- return $this->name;
+ $path = Util::joinPaths(
+ $basePath,
+ static::DEFAULT_SOURCE,
+ explode('/', $name)
+ );
+ parent::__construct($basePath, $path, $name);
}
/**
* Get full path to the root install for this project
*
+ * @deprecated 1.3..2.0 use getPath() instead
* @param string $base Rewrite root (or 'vendor' for actual module path)
* @return string Path for this module
*/
public function getModulePath($base = self::DEFAULT_SOURCE)
{
- return Util::joinPaths(
- $this->basePath,
- $base,
- explode('/', $this->name)
- );
- }
-
- /**
- * Get json content for this module from composer.json
- *
- * @return array
- */
- protected function getJson()
- {
- $composer = Util::joinPaths($this->getModulePath(), 'composer.json');
- $file = new JsonFile($composer);
- return $file->read();
- }
-
- /**
- * Expose all web accessible paths for this module
- *
- * @param ExposeMethod $method
- * @param string $target Replacement target for 'vendor' prefix to rewrite to. Defaults to 'resources'
- */
- public function exposePaths(ExposeMethod $method, $target = self::DEFAULT_TARGET)
- {
- $folders = $this->getExposedFolders();
- $sourcePath = $this->getModulePath(self::DEFAULT_SOURCE);
- $targetPath = $this->getModulePath($target);
- foreach ($folders as $folder) {
- // Get paths for this folder and delegate to expose method
- $folderSourcePath = Util::joinPaths($sourcePath, $folder);
- $folderTargetPath = Util::joinPaths($targetPath, $folder);
- $method->exposeDirectory($folderSourcePath, $folderTargetPath);
- }
- }
-
- /**
- * Get name of all folders to expose (relative to module root)
- *
- * @return array
- */
- public function getExposedFolders()
- {
- $data = $this->getJson();
-
- // Only expose if correct type
- if (empty($data['type']) || $data['type'] !== VendorPlugin::MODULE_TYPE) {
- return [];
- }
-
- // Get all dirs to expose
- if (empty($data['extra']['expose'])) {
- return [];
- }
- $expose = $data['extra']['expose'];
-
- // Validate all paths are safe
- foreach ($expose as $exposeFolder) {
- if (!$this->validateFolder($exposeFolder)) {
- throw new LogicException("Invalid module folder " . $exposeFolder);
- }
- }
- return $expose;
- }
-
- /**
- * Validate the given folder is allowed
- *
- * @param string $exposeFolder Relative folder name to check
- * @return bool
- */
- protected function validateFolder($exposeFolder)
- {
- if (strstr($exposeFolder, '.')) {
- return false;
- }
- if (strpos($exposeFolder, '/') === 0) {
- return false;
- }
- if (strpos($exposeFolder, '\\') === 0) {
- return false;
+ if ($base === self::DEFAULT_TARGET) {
+ return $this->getPublicPath();
+ } else {
+ return $this->getPath();
}
- return true;
}
}
diff --git a/src/VendorPlugin.php b/src/VendorPlugin.php
index e48969c..2e34d68 100644
--- a/src/VendorPlugin.php
+++ b/src/VendorPlugin.php
@@ -9,11 +9,13 @@
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Factory;
use Composer\Installer\PackageEvent;
+use Composer\Installers\Installer;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Plugin\Capability\CommandProvider;
use Composer\Plugin\Capable;
use Composer\Plugin\PluginInterface;
+use Composer\Script\Event;
use Composer\Util\Filesystem;
use SilverStripe\VendorPlugin\Console\VendorCommandProvider;
@@ -23,10 +25,17 @@
class VendorPlugin implements PluginInterface, EventSubscriberInterface, Capable
{
/**
- * Module type to match
+ * Default module type
+ *
+ * @deprecated 1.3..2.0 Use MODULE_FILTER instead
*/
const MODULE_TYPE = 'silverstripe-vendormodule';
+ /**
+ * Filter for matching library types to expose
+ */
+ const MODULE_FILTER = '/^silverstripe\-(\w+)$/';
+
/**
* Method env var to query
*/
@@ -78,29 +87,43 @@ public static function getSubscribedEvents()
'post-package-update' => 'installPackage',
'post-package-install' => 'installPackage',
'pre-package-uninstall' => 'uninstallPackage',
+ 'post-install-cmd' => 'installRootPackage',
+ 'post-update-cmd' => 'installRootPackage',
];
}
/**
* Get vendor module instance for this event
*
+ * @deprecated 1.3..2.0
* @param PackageEvent $event
- * @return VendorModule
+ * @return Library|null
*/
protected function getVendorModule(PackageEvent $event)
+ {
+ return $this->getLibrary($event);
+ }
+
+ /**
+ * Gets library being installed
+ *
+ * @param PackageEvent $event
+ * @return Library|null
+ */
+ public function getLibrary(PackageEvent $event)
{
// Ensure package is the valid type
$package = $this->getOperationPackage($event);
- if (!$package || $package->getType() !== self::MODULE_TYPE) {
+ if (!$package || !preg_match(self::MODULE_FILTER, $package->getType())) {
return null;
}
- // Find project path
- $projectPath = $this->getProjectPath();
- $name = $package->getName();
+ // Get appropriate installer and query install path
+ $installer = $event->getComposer()->getInstallationManager()->getInstaller($package->getType());
+ $path = $installer->getInstallPath($package);
// Build module
- return new VendorModule($projectPath, $name);
+ return new Library($this->getProjectPath(), $path);
}
/**
@@ -110,18 +133,34 @@ protected function getVendorModule(PackageEvent $event)
*/
public function installPackage(PackageEvent $event)
{
- // Ensure module exists and has any folders to expose
- $module = $this->getVendorModule($event);
- if (!$module || !$module->getExposedFolders()) {
+ // Ensure module exists and requires exposure
+ $library = $this->getLibrary($event);
+ if (!$library) {
return;
}
- // Run with task
- $task = new VendorExposeTask($this->getProjectPath(), $this->filesystem, VendorModule::DEFAULT_TARGET);
- $task->process($event->getIO(), [$module]);
+ // Install found library
+ $this->installLibrary($event->getIO(), $library);
}
/**
+ * Install resources from the root package
+ *
+ * @param Event $event
+ */
+ public function installRootPackage(Event $event)
+ {
+ // Build library in base path
+ $basePath = $this->getProjectPath();
+ $library = new Library($basePath, $basePath);
+
+ // Pass to library installer
+ $this->installLibrary($event->getIO(), $library);
+ }
+
+ /**
+ * Get base path to project
+ *
* @return string
*/
protected function getProjectPath()
@@ -136,27 +175,27 @@ protected function getProjectPath()
*/
public function uninstallPackage(PackageEvent $event)
{
- // Ensure package is the valid type
- $module = $this->getVendorModule($event);
- if (!$module) {
+ // Check if library exists and exposes any directories
+ $library = $this->getLibrary($event);
+ if (!$library || !$library->requiresExpose()) {
return;
}
// Check path to remove
- $target = $module->getModulePath(VendorModule::DEFAULT_TARGET);
+ $target = $library->getPublicPath();
if (!is_dir($target)) {
return;
}
// Remove directory
- $name = $module->getName();
+ $name = $library->getName();
$event->getIO()->write("Removing web directories for module {$name}:");
$this->filesystem->removeDirectory($target);
// Cleanup empty vendor dir if this is the last module
- $vendorTarget = dirname($target);
- if ($this->filesystem->isDirEmpty($vendorTarget)) {
- $this->filesystem->removeDirectory($vendorTarget);
+ $targetParent = dirname($target);
+ if ($this->filesystem->isDirEmpty($targetParent)) {
+ $this->filesystem->removeDirectory($targetParent);
}
}
@@ -187,4 +226,25 @@ public function getCapabilities()
CommandProvider::class => VendorCommandProvider::class
];
}
+
+ /**
+ * Expose the given Library object
+ *
+ * @param IOInterface $IO
+ * @param Library $library
+ */
+ protected function installLibrary(IOInterface $IO, Library $library)
+ {
+ if (!$library || !$library->requiresExpose()) {
+ return;
+ }
+
+ // Create exposure task
+ $task = new VendorExposeTask(
+ $this->getProjectPath(),
+ $this->filesystem,
+ $library->getBasePublicPath()
+ );
+ $task->process($IO, [$library]);
+ }
}