Compare commits

...

23 Commits
1.0.0 ... 1

Author SHA1 Message Date
Steve Boyd acb067fdc2
MNT Use gha-dispatch-ci (#32) 2023-03-23 14:15:48 +13:00
Steve Boyd e5dceb4546
API Update deprecations (#27)
* ENH Update deprecation messages

* API Update deprecations
2022-10-25 17:23:26 +13:00
Steve Boyd c4e8728ff9 Merge branch '1.7' into 1 2022-08-03 14:32:59 +12:00
Guy Sartorelli da89aa5dcc
Merge pull request #24 from creative-commoners/pulls/1.7/standardise-modules
MNT Standardise modules
2022-08-02 15:32:19 +12:00
Steve Boyd 4ec7b15347 MNT Standardise modules 2022-08-01 10:45:07 +12:00
Steve Boyd 2f74096ea4 Merge branch '1.7' into 1 2022-07-26 16:42:52 +12:00
Guy Sartorelli 2175573ca5
Merge pull request #23 from creative-commoners/pulls/1.7/module-standards
MNT Use GitHub Actions CI
2022-07-15 17:01:25 +12:00
Steve Boyd f68a4cc2be MNT Use GitHub Actions CI 2022-07-06 14:02:51 +12:00
Andrew Paxley 301daf2437
FIX Only write composer.json if there are changes (#21)
FIX Only write composer.json if there are changes
2022-05-10 09:40:14 +12:00
Guy Sartorelli 8a77da94d1
Merge pull request #20 from creative-commoners/pulls/1/php81
ENH PHP 8.1 compatibility
2022-04-22 16:16:42 +12:00
Steve Boyd 02f5008b46 ENH PHP 8.1 compatibility 2022-04-13 17:43:58 +12:00
Jordi Boggiano a76509e8a6
FIX Remove unnecessary methods from trait, adds Composer 2.3 compatibility (#19) 2022-04-01 21:25:06 +13:00
Steve Boyd 102c2bc100
Remove build status badge 2021-01-21 16:06:24 +13:00
Steve Boyd 68c7402854
Update build status badge 2021-01-21 16:05:34 +13:00
Steve Boyd da7ccb0a09
Merge pull request #17 from creative-commoners/pull/1/deps-pipe
DEP Use double pipe for requirements
2020-11-11 16:37:26 +13:00
Steve Boyd 01c9b45185 DEP Use double pipe for requirements 2020-11-11 16:15:16 +13:00
Maxime Rainville 374f223a17
API Add support for Composer 2 (#16) 2020-10-26 13:55:21 +13:00
Robbie Averill 88cd7ed3a0
Add editorconfig rule for composer.json to be allowed four spaces 2018-04-11 19:22:09 +12:00
Damian Mooyman 96cd4972a0
Update 1 branch alias 2018-04-11 15:10:48 +12:00
Damian Mooyman eacf7e3c06
Merge pull request #7 from open-sausages/pulls/1/appy-days
Support legacy rewrite for projects with mysite folder
2018-04-11 15:04:12 +12:00
Damian Mooyman 9ab71cf1e9
Support legacy rewrite for projects with mysite folder 2018-04-05 16:35:24 +12:00
Aaron Carlino 3e16a5b138
Merge pull request #4 from open-sausages/pulls/1/throwing-a-public
API Support `public-files`
2018-01-12 14:33:30 +13:00
Damian Mooyman a959a68e54 API Support `public-files` 2018-01-10 10:14:52 +13:00
10 changed files with 278 additions and 88 deletions

View File

@ -13,3 +13,6 @@ insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[composer.json]
indent_size = 4

11
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,11 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
ci:
name: CI
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1

16
.github/workflows/dispatch-ci.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: Dispatch CI
on:
# At 12:00 PM UTC, only on Friday and Saturday
schedule:
- cron: '0 12 * * 5,6'
jobs:
dispatch-ci:
name: Dispatch CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Dispatch CI
uses: silverstripe/gha-dispatch-ci@v1

17
.github/workflows/keepalive.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Keepalive
on:
workflow_dispatch:
# The 4th of every month at 10:50am UTC
schedule:
- cron: '50 10 4 * *'
jobs:
keepalive:
name: Keepalive
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Keepalive
uses: silverstripe/gha-keepalive@v1

View File

@ -1,4 +1,7 @@
# SilverStripe recipe-plugin
# Silverstripe recipe-plugin
[![CI](https://github.com/silverstripe/recipe-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/recipe-plugin/actions/workflows/ci.yml)
[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)
## Introduction
@ -133,6 +136,9 @@ Recipe types should follow the following rules:
- The `require` must have `silverstripe/recipe-plugin` as a dependency.
- `extra.project-files` must be declared as a list of wildcard patterns, matching the files in the recipe root
as they should be copied to the root project. The relative paths of these resources are equivalent.
- `extra.public-files` must be declared for any files which should be copied to the `public` web folder. If the project
in question doesn't have any public folder, these will be copied to root instead. Note that all public files
must be committed to the recipe `public` folder.
An example recipe:
@ -151,9 +157,31 @@ An example recipe:
"project-files": [
"mysite/_config/*.yml",
"mysite/code/MyBlogPage.php"
"client/src/*"
],
"public-files": [
"client/dist/*"
]
},
"prefer-stable": true,
"minimum-stability": "dev"
}
```
The files within this recipe would be organised in the structure:
```
client/
src/
blog.scss
mysite/
_config/
settings.yml
code/
MyBlogPage.php
public/
client/
dist/
blog.css
composer.json
```

View File

@ -17,11 +17,16 @@
"extra": {
"class": "SilverStripe\\RecipePlugin\\RecipePlugin"
},
"scripts": {
"lint": "phpcs src/",
"lint-clean": "phpcbf src/"
},
"require": {
"composer-plugin-api": "^1.1"
"composer-plugin-api": "^1.1 || ^2"
},
"require-dev": {
"composer/composer": "^1.2"
"composer/composer": "^1.2 || 2",
"squizlabs/php_codesniffer": "^3.5"
},
"minimum-stability": "dev"
}

12
phpcs.xml.dist Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="SilverStripe">
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
<file>src</file>
<!-- base rules are PSR-12 -->
<rule ref="PSR12" >
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps" />
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols" />
</rule>
</ruleset>

View File

@ -14,31 +14,6 @@ use Symfony\Component\Console\Output\OutputInterface;
trait RecipeCommandBehaviour
{
/**
* Gets the application instance for this command.
*
* @return Application An Application instance
*/
public abstract function getApplication();
/**
* @param bool $required
* @param bool|null $disablePlugins
* @throws \RuntimeException
* @return Composer
*/
public abstract function getComposer($required = true, $disablePlugins = null);
/**
* Removes the cached composer instance
*/
public abstract function resetComposer();
/**
* @return IOInterface
*/
abstract public function getIO();
/**
* @param OutputInterface $output
* @param string $recipe
@ -128,17 +103,17 @@ trait RecipeCommandBehaviour
}
// Existing version is already a ^1.0.0 or ~1.0.0 constraint
if (preg_match('#^[~^]#', $existingVersion)) {
if (preg_match('#^[~^]#', $existingVersion ?? '')) {
return $existingVersion;
}
// Existing version is already a dev constraint
if (stristr($existingVersion, 'dev') !== false) {
if (stristr($existingVersion ?? '', 'dev') !== false) {
return $existingVersion;
}
// Numeric-only version maps to semver constraint
if (preg_match('#^([\d.]+)$#', $existingVersion)) {
if (preg_match('#^([\d.]+)$#', $existingVersion ?? '')) {
return "^{$existingVersion}";
}
@ -242,7 +217,7 @@ trait RecipeCommandBehaviour
// Add new require / extra-installed
$composerData['require'] = $require;
if ($previouslyInstalled){
if ($previouslyInstalled) {
if (!isset($composerData['extra'])) {
$composerData['extra'] = [];
}

View File

@ -14,9 +14,15 @@ use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
class RecipeInstaller extends LibraryInstaller {
class RecipeInstaller extends LibraryInstaller
{
/**
* @var bool
*/
private $hasWrittenFiles = false;
public function __construct(IOInterface $io, Composer $composer) {
public function __construct(IOInterface $io, Composer $composer)
{
parent::__construct($io, $composer, null);
}
@ -27,69 +33,100 @@ class RecipeInstaller extends LibraryInstaller {
* @param string $sourceRoot Base of source files (no trailing slash)
* @param string $destinationRoot Base of destination directory (no trailing slash)
* @param array $filePatterns List of file patterns in wildcard format (e.g. `code/My*.php`)
* @param string $registrationKey Registration key for installed files
* @param string $name Name of project file type being installed
*/
protected function installProjectFiles($recipe, $sourceRoot, $destinationRoot, $filePatterns)
{
protected function installProjectFiles(
$recipe,
$sourceRoot,
$destinationRoot,
$filePatterns,
$registrationKey,
$name = 'project'
) {
// load composer json data
$composerFile = new JsonFile(Factory::getComposerFile(), null, $this->io);
$composerData = $composerFile->read();
$installedFiles = isset($composerData['extra'][RecipePlugin::PROJECT_FILES_INSTALLED])
? $composerData['extra'][RecipePlugin::PROJECT_FILES_INSTALLED]
$installedFiles = isset($composerData['extra'][$registrationKey])
? $composerData['extra'][$registrationKey]
: [];
// Load all project files
$fileIterator = $this->getFileIterator($sourceRoot, $filePatterns);
$any = false;
foreach($fileIterator as $path => $info) {
$destination = $destinationRoot . substr($path, strlen($sourceRoot));
$relativePath = substr($path, strlen($sourceRoot) + 1); // Name path without leading '/'
// Write header
foreach ($fileIterator as $path => $info) {
// Write header on first file
if (!$any) {
$this->io->write("Installing project files for recipe <info>{$recipe}</info>:");
$this->io->write("Installing {$name} files for recipe <info>{$recipe}</info>:");
$any = true;
}
// Check if file exists
if (file_exists($destination)) {
if (file_get_contents($destination) === file_get_contents($path)) {
$this->io->write(
" - Skipping <info>$relativePath</info> (<comment>existing, but unchanged</comment>)"
);
} else {
$this->io->write(
" - Skipping <info>$relativePath</info> (<comment>existing and modified in project</comment>)"
);
}
} elseif (in_array($relativePath, $installedFiles)) {
// Don't re-install previously installed files that have been deleted
$this->io->write(
" - Skipping <info>$relativePath</info> (<comment>previously installed</comment>)"
);
} else {
$any++;
$this->io->write(" - Copying <info>$relativePath</info>");
$this->filesystem->ensureDirectoryExists(dirname($destination));
copy($path, $destination);
}
// Install this file
$relativePath = $this->installProjectFile($sourceRoot, $destinationRoot, $path, $installedFiles);
// Add file to installed (even if already exists)
if (!in_array($relativePath, $installedFiles)) {
if (!in_array($relativePath, $installedFiles ?? [])) {
$installedFiles[] = $relativePath;
}
}
// If any files are written, modify composer.json with newly installed files
if ($installedFiles) {
if ($this->hasWrittenFiles) {
sort($installedFiles);
if (!isset($composerData['extra'])) {
$composerData['extra'] = [];
}
$composerData['extra'][RecipePlugin::PROJECT_FILES_INSTALLED] = $installedFiles;
$composerData['extra'][$registrationKey] = $installedFiles;
$composerFile->write($composerData);
// Reset the variable so that we can try this trick again later
$this->hasWrittenFiles = false;
}
}
/**
* @param string $sourceRoot Base of source files (no trailing slash)
* @param string $destinationRoot Base of destination directory (no trailing slash)
* @param string $sourcePath Full filesystem path to the file to copy
* @param array $installedFiles List of installed files
* @return bool|string
*/
protected function installProjectFile($sourceRoot, $destinationRoot, $sourcePath, $installedFiles)
{
// Relative path
$relativePath = substr($sourcePath ?? '', strlen($sourceRoot ?? '') + 1); // Name path without leading '/'
// Get destination path
$relativeDestination = $this->rewriteFilePath($destinationRoot, $relativePath);
$destination = $destinationRoot . DIRECTORY_SEPARATOR . $relativeDestination;
// Check if file exists
if (file_exists($destination ?? '')) {
if (file_get_contents($destination ?? '') === file_get_contents($sourcePath ?? '')) {
$this->io->write(
" - Skipping <info>$relativePath</info> (<comment>existing, but unchanged</comment>)"
);
} else {
$this->io->write(
" - Skipping <info>$relativePath</info> (<comment>existing and modified in project</comment>)"
);
}
} elseif (
in_array($relativePath, $installedFiles ?? []) ||
in_array($relativeDestination, $installedFiles ?? [])
) {
// Don't re-install previously installed files that have been deleted
$this->io->write(
" - Skipping <info>$relativePath</info> (<comment>previously installed</comment>)"
);
} else {
$this->io->write(" - Copying <info>$relativePath</info>");
$this->filesystem->ensureDirectoryExists(dirname($destination ?? ''));
copy($sourcePath ?? '', $destination ?? '');
$this->hasWrittenFiles = true;
}
return $relativePath;
}
/**
* Get iterator of matching source files to copy
*
@ -97,13 +134,14 @@ class RecipeInstaller extends LibraryInstaller {
* @param array $patterns List of wildcard patterns to match
* @return Iterator File iterator, where key is path and value is file info object
*/
protected function getFileIterator($sourceRoot, $patterns) {
protected function getFileIterator($sourceRoot, $patterns)
{
// Build regexp pattern
$expressions = [];
foreach($patterns as $pattern) {
foreach ($patterns as $pattern) {
$expressions[] = $this->globToRegexp($pattern);
}
$regExp = '#^' . $this->globToRegexp($sourceRoot . '/').'(('.implode(')|(', $expressions).'))$#';
$regExp = '#^' . $this->globToRegexp($sourceRoot . '/') . '((' . implode(')|(', $expressions) . '))$#';
// Build directory iterator
$directoryIterator = new RecursiveDirectoryIterator(
@ -125,11 +163,12 @@ class RecipeInstaller extends LibraryInstaller {
* @param string $glob
* @return string
*/
protected function globToRegexp($glob) {
$sourceParts = explode('*', $glob);
$regexParts = array_map(function($part) {
return preg_quote($part, '#');
}, $sourceParts);
protected function globToRegexp($glob)
{
$sourceParts = explode('*', $glob ?? '');
$regexParts = array_map(function ($part) {
return preg_quote($part ?? '', '#');
}, $sourceParts ?? []);
return implode('(.+)', $regexParts);
}
@ -143,19 +182,77 @@ class RecipeInstaller extends LibraryInstaller {
return;
}
// Find recipe base dir
$recipePath = $this->getInstallPath($package);
// Find project path
$destinationPath = dirname(realpath(Factory::getComposerFile()));
$projectPath = dirname(realpath(Factory::getComposerFile() ?? '') ?? '');
// Find public path
$candidatePublicPath = $projectPath . DIRECTORY_SEPARATOR . RecipePlugin::PUBLIC_PATH;
$publicPath = is_dir($candidatePublicPath ?? '') ? $candidatePublicPath : $projectPath;
// Copy project files to root
$name = $package->getName();
$extra = $package->getExtra();
// Install project-files
if (isset($extra[RecipePlugin::PROJECT_FILES])) {
$this->installProjectFiles(
$name,
$this->getInstallPath($package),
$destinationPath,
$extra[RecipePlugin::PROJECT_FILES]
$recipePath,
$projectPath,
$extra[RecipePlugin::PROJECT_FILES],
RecipePlugin::PROJECT_FILES_INSTALLED,
'project'
);
}
// Install public-files
if (isset($extra[RecipePlugin::PUBLIC_FILES])) {
$this->installProjectFiles(
$name,
$recipePath . '/' . RecipePlugin::PUBLIC_PATH,
$publicPath,
$extra[RecipePlugin::PUBLIC_FILES],
RecipePlugin::PUBLIC_FILES_INSTALLED,
'public'
);
}
}
/**
* Perform any file rewrites necessary to a relative path of a file being installed.
* E.g. if 'mysite' folder exists, rewrite 'mysite' to 'app' and 'mysite/code' to 'app/src'
*
* This will be removed in 2.0 as the app folder will be hard coded and no rewrites supported.
*
* @deprecated 1.2.0 Will be removed without equivalent functionality to replace it
* @param string $destinationRoot Project root
* @param string $relativePath Relative path to the resource being installed
* @return string Relative path we should write to
*/
protected function rewriteFilePath($destinationRoot, $relativePath)
{
// If app folder exists, no rewrite
if (is_dir($destinationRoot . DIRECTORY_SEPARATOR . 'app')) {
return $relativePath;
}
// if mysite folder does NOT exist, no rewrite
if (!is_dir($destinationRoot . DIRECTORY_SEPARATOR . 'mysite')) {
return $relativePath;
}
// Return first rewrite
$rewrites = [
'app/src' => 'mysite/code',
'app' => 'mysite',
];
foreach ($rewrites as $from => $to) {
if (stripos($relativePath ?? '', $from ?? '') === 0) {
return $to . substr($relativePath ?? '', strlen($from ?? ''));
}
}
return $relativePath;
}
}

View File

@ -1,6 +1,5 @@
<?php
namespace SilverStripe\RecipePlugin;
use Composer\Composer;
@ -27,22 +26,37 @@ class RecipePlugin implements PluginInterface, EventSubscriberInterface, Capable
/**
* Type of recipe to check for
*/
const RECIPE_TYPE = 'silverstripe-recipe';
public const RECIPE_TYPE = 'silverstripe-recipe';
/**
* 'extra' key for project files
*/
const PROJECT_FILES = 'project-files';
public const PROJECT_FILES = 'project-files';
/**
* 'extra' key for public files
*/
public const PUBLIC_FILES = 'public-files';
/**
* Hard-coded 'public' web-root folder
*/
public const PUBLIC_PATH = 'public';
/**
* 'extra' key for list of project files installed
*/
const PROJECT_FILES_INSTALLED = 'project-files-installed';
public const PROJECT_FILES_INSTALLED = 'project-files-installed';
/**
* 'extra' key for list of public files installed
*/
public const PUBLIC_FILES_INSTALLED = 'public-files-installed';
/**
* 'extra' key for project dependencies installed
*/
const PROJECT_DEPENDENCIES_INSTALLED = 'project-dependencies-installed';
public const PROJECT_DEPENDENCIES_INSTALLED = 'project-dependencies-installed';
public function activate(Composer $composer, IOInterface $io)
{
@ -81,8 +95,11 @@ class RecipePlugin implements PluginInterface, EventSubscriberInterface, Capable
$file = new JsonFile(Factory::getComposerFile());
$data = $file->read();
// Remove project-files from project, and any empty extra
unset($data['extra']['project-files']);
// Remove project and public files from project
unset($data['extra'][self::PROJECT_FILES]);
unset($data['extra'][self::PUBLIC_FILES]);
// Remove redundant empty extra
if (empty($data['extra'])) {
unset($data['extra']);
}
@ -115,4 +132,13 @@ class RecipePlugin implements PluginInterface, EventSubscriberInterface, Capable
CommandProvider::class => RecipeCommandProvider::class
];
}
public function deactivate(Composer $composer, IOInterface $io)
{
}
public function uninstall(Composer $composer, IOInterface $io)
{
}
}