diff --git a/composer.lock b/composer.lock
index 5dc6582c42..70db1da84a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1401,16 +1401,16 @@
},
{
"name": "psr/log",
- "version": "1.0.2",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
- "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"shasum": ""
},
"require": {
@@ -1444,7 +1444,7 @@
"psr",
"psr-3"
],
- "time": "2016-10-10T12:19:37+00:00"
+ "time": "2018-11-20T15:27:04+00:00"
},
{
"name": "stack/builder",
@@ -3379,16 +3379,16 @@
},
{
"name": "composer/composer",
- "version": "1.8.4",
+ "version": "1.8.5",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "bc364c2480c17941e2135cfc568fa41794392534"
+ "reference": "949b116f9e7d98d8d276594fed74b580d125c0e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/bc364c2480c17941e2135cfc568fa41794392534",
- "reference": "bc364c2480c17941e2135cfc568fa41794392534",
+ "url": "https://api.github.com/repos/composer/composer/zipball/949b116f9e7d98d8d276594fed74b580d125c0e6",
+ "reference": "949b116f9e7d98d8d276594fed74b580d125c0e6",
"shasum": ""
},
"require": {
@@ -3455,28 +3455,27 @@
"dependency",
"package"
],
- "time": "2019-02-11T09:52:10+00:00"
+ "time": "2019-04-09T15:46:48+00:00"
},
{
"name": "composer/spdx-licenses",
- "version": "1.5.0",
+ "version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
- "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2"
+ "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
- "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d",
+ "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0"
+ "php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
- "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
},
"type": "library",
"extra": {
@@ -3516,7 +3515,7 @@
"spdx",
"validator"
],
- "time": "2018-11-01T09:45:54+00:00"
+ "time": "2019-03-26T10:23:26+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -3995,12 +3994,12 @@
"version": "v1.6.5",
"source": {
"type": "git",
- "url": "https://github.com/mikey179/vfsStream.git",
+ "url": "https://github.com/bovigo/vfsStream.git",
"reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
+ "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
"reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
"shasum": ""
},
@@ -5210,7 +5209,7 @@
},
{
"name": "symfony/filesystem",
- "version": "v4.2.4",
+ "version": "v4.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
@@ -5260,7 +5259,7 @@
},
{
"name": "symfony/finder",
- "version": "v4.2.4",
+ "version": "v4.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
diff --git a/core/lib/Drupal/Component/Scaffold/CommandProvider.php b/core/lib/Drupal/Component/Scaffold/CommandProvider.php
index 22350a9b97..d78b486a8c 100644
--- a/core/lib/Drupal/Component/Scaffold/CommandProvider.php
+++ b/core/lib/Drupal/Component/Scaffold/CommandProvider.php
@@ -5,7 +5,7 @@
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
/**
- * Lists composer commands provided by this package.
+ * List of all commands provided by this package.
*/
class CommandProvider implements CommandProviderCapability {
@@ -14,7 +14,7 @@ class CommandProvider implements CommandProviderCapability {
*/
public function getCommands() {
return [
- new DrupalScaffoldCommand(),
+ new ComposerScaffoldCommand(),
];
}
diff --git a/core/lib/Drupal/Component/Scaffold/DrupalScaffoldCommand.php b/core/lib/Drupal/Component/Scaffold/ComposerScaffoldCommand.php
similarity index 67%
rename from core/lib/Drupal/Component/Scaffold/DrupalScaffoldCommand.php
rename to core/lib/Drupal/Component/Scaffold/ComposerScaffoldCommand.php
index 14498bf7c8..f5b792ad1c 100644
--- a/core/lib/Drupal/Component/Scaffold/DrupalScaffoldCommand.php
+++ b/core/lib/Drupal/Component/Scaffold/ComposerScaffoldCommand.php
@@ -7,11 +7,11 @@
use Symfony\Component\Console\Output\OutputInterface;
/**
- * The "drupal:scaffold" command class.
+ * The "composer:scaffold" command class.
*
- * Downloads scaffold files and generates the autoload.php file.
+ * Composer scaffold files and generates the autoload.php file.
*/
-class DrupalScaffoldCommand extends BaseCommand {
+class ComposerScaffoldCommand extends BaseCommand {
/**
* {@inheritdoc}
@@ -19,8 +19,8 @@ class DrupalScaffoldCommand extends BaseCommand {
protected function configure() {
parent::configure();
$this
- ->setName('drupal:scaffold')
- ->setDescription('Update the Drupal scaffold files.');
+ ->setName('composer:scaffold')
+ ->setDescription('Update the Composer scaffold files.');
}
/**
@@ -28,7 +28,7 @@ protected function configure() {
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$handler = new Handler($this->getComposer(), $this->getIO());
- $handler->downloadScaffold();
+ $handler->scaffold();
// Generate the autoload.php file after generating the scaffold files.
$handler->generateAutoload();
}
diff --git a/core/lib/Drupal/Component/Scaffold/FileFetcher.php b/core/lib/Drupal/Component/Scaffold/FileFetcher.php
deleted file mode 100644
index c5907427ff..0000000000
--- a/core/lib/Drupal/Component/Scaffold/FileFetcher.php
+++ /dev/null
@@ -1,136 +0,0 @@
-remoteFilesystem = $remoteFilesystem;
- $this->io = $io;
- $this->source = $source;
- // TODO: this should be injectable.
- $this->fs = new Filesystem();
- $this->progress = $progress;
- }
-
- /**
- * Downloads all required files and writes it to the file system.
- *
- * @param string $version
- * The version of the scaffold file to be retrieved.
- * @param string $destination
- * The location on the filesystem where we will place the file.
- * @param bool $override
- * Whether the file should be overridden or left in place.
- */
- public function fetch($version, $destination, $override) {
- foreach ($this->filenames as $sourceFilename => $filename) {
- $target = "$destination/$filename";
- if ($override || !file_exists($target)) {
- $url = $this->getUri($sourceFilename, $version);
- $this->fs->ensureDirectoryExists($destination . '/' . dirname($filename));
- if ($this->progress) {
- $this->io->writeError(" - $filename ($url): ", FALSE);
- $this->remoteFilesystem->copy($url, $url, $target, $this->progress);
- // Used to put a new line because the remote file system does not put
- // one.
- $this->io->writeError('');
- }
- else {
- $this->remoteFilesystem->copy($url, $url, $target, $this->progress);
- }
- }
- }
- }
-
- /**
- * Set filenames.
- *
- * @param array $filenames
- * An array of filenames to retrieve from the drupal git repository.
- */
- public function setFilenames(array $filenames) {
- $this->filenames = $filenames;
- }
-
- /**
- * Replace filename and version in the source pattern with their values.
- *
- * @param string $filename
- * The filename to retrieve.
- * @param string $version
- * The version of the git drupal repository.
- *
- * @return string
- * A uri of the filename/version combination.
- */
- protected function getUri($filename, $version) {
- $map = [
- '{path}' => $filename,
- '{version}' => $version,
- ];
- return str_replace(array_keys($map), array_values($map), $this->source);
- }
-
-}
diff --git a/core/lib/Drupal/Component/Scaffold/Handler.php b/core/lib/Drupal/Component/Scaffold/Handler.php
index 5d49c64ae8..b05380dc3b 100644
--- a/core/lib/Drupal/Component/Scaffold/Handler.php
+++ b/core/lib/Drupal/Component/Scaffold/Handler.php
@@ -1,324 +1,211 @@
['tests', 'driver-testsuite'],
- 'behat/mink-browserkit-driver' => ['tests'],
- 'behat/mink-goutte-driver' => ['tests'],
- 'drupal/coder' => ['coder_sniffer/Drupal/Test', 'coder_sniffer/DrupalPractice/Test'],
- 'doctrine/cache' => ['tests'],
- 'doctrine/collections' => ['tests'],
- 'doctrine/common' => ['tests'],
- 'doctrine/inflector' => ['tests'],
- 'doctrine/instantiator' => ['tests'],
- 'egulias/email-validator' => ['documentation', 'tests'],
- 'fabpot/goutte' => ['Goutte/Tests'],
- 'guzzlehttp/promises' => ['tests'],
- 'guzzlehttp/psr7' => ['tests'],
- 'jcalderonzumba/gastonjs' => ['docs', 'examples', 'tests'],
- 'jcalderonzumba/mink-phantomjs-driver' => ['tests'],
- 'masterminds/html5' => ['test'],
- 'mikey179/vfsStream' => ['src/test'],
- 'paragonie/random_compat' => ['tests'],
- 'phpdocumentor/reflection-docblock' => ['tests'],
- 'phpunit/php-code-coverage' => ['tests'],
- 'phpunit/php-timer' => ['tests'],
- 'phpunit/php-token-stream' => ['tests'],
- 'phpunit/phpunit' => ['tests'],
- 'phpunit/php-mock-objects' => ['tests'],
- 'sebastian/comparator' => ['tests'],
- 'sebastian/diff' => ['tests'],
- 'sebastian/environment' => ['tests'],
- 'sebastian/exporter' => ['tests'],
- 'sebastian/global-state' => ['tests'],
- 'sebastian/recursion-context' => ['tests'],
- 'stack/builder' => ['tests'],
- 'symfony/browser-kit' => ['Tests'],
- 'symfony/class-loader' => ['Tests'],
- 'symfony/console' => ['Tests'],
- 'symfony/css-selector' => ['Tests'],
- 'symfony/debug' => ['Tests'],
- 'symfony/dependency-injection' => ['Tests'],
- 'symfony/dom-crawler' => ['Tests'],
- // @see \Drupal\Tests\Component\EventDispatcher\ContainerAwareEventDispatcherTest
- // 'symfony/event-dispatcher' => ['Tests'],
- 'symfony/http-foundation' => ['Tests'],
- 'symfony/http-kernel' => ['Tests'],
- 'symfony/process' => ['Tests'],
- 'symfony/psr-http-message-bridge' => ['Tests'],
- 'symfony/routing' => ['Tests'],
- 'symfony/serializer' => ['Tests'],
- 'symfony/translation' => ['Tests'],
- 'symfony/validator' => ['Tests', 'Resources'],
- 'symfony/yaml' => ['Tests'],
- 'symfony-cmf/routing' => ['Test', 'Tests'],
- 'twig/twig' => ['doc', 'ext', 'test'],
- ];
+ protected $allowedPackages;
/**
* Handler constructor.
*
* @param \Composer\Composer $composer
- * The primary composer application object.
+ * The Composer service.
* @param \Composer\IO\IOInterface $io
- * The composer IO object for printing messages to the console.
+ * The Composer I/O service.
*/
public function __construct(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
- $this->progress = TRUE;
-
- // Pre-load all of the plugins classes so that when we update the
- // drupal/drupal-scaffold plugin itself, we do not run into issues with
- // api mismatches between versions.
- $this->manualLoad();
}
/**
- * Pre-load all of our sources on initial load.
+ * Post install command event to execute the scaffolding.
*
- * This ensures that we will not get a more recent version of one of
- * our classes e.g. after a 'composer update' operation.
+ * @param \Composer\Script\Event $event
+ * The Composer event.
*/
- protected function manualLoad() {
- $src_dir = __DIR__;
-
- $classes = [
- 'CommandProvider',
- 'DrupalScaffoldCommand',
- 'FileFetcher',
- 'PrestissimoFileFetcher',
- ];
-
- foreach ($classes as $src) {
- if (!class_exists('\\Drupal\\Component\\Scaffold\\' . $src)) {
- include "{$src_dir}/{$src}.php";
- }
- }
+ public function onPostCmdEvent(Event $event) {
+ $this->scaffold();
}
/**
- * Determines if drupal/core is being changed by the install or update.
+ * Gets the array of file mappings provided by a given package.
*
- * @param \Composer\DependencyResolver\Operation\OperationInterface $operation
- * Determines what type of Composer package operation is occurring.
+ * @param \Composer\Package\PackageInterface $package
+ * The Composer package from which to get the file mappings.
*
- * @return mixed
- * Returns 'drupal/core' if core is being updated, otherwise returns null.
+ * @return array
+ * An associative array of file mappings, keyed by relative source file
+ * path. Items that are specified as 'false' are converted to an empty string.
+ * For example:
+ * [
+ * 'path/to/source/file' => 'path/to/destination',
+ * 'path/to/source/file' => '',
+ * ]
*/
- protected function getCorePackage(OperationInterface $operation) {
- if ($operation instanceof InstallOperation) {
- $package = $operation->getPackage();
- }
- elseif ($operation instanceof UpdateOperation) {
- $package = $operation->getTargetPackage();
- }
- if (isset($package) && $package instanceof PackageInterface && $package->getName() == 'drupal/core') {
- return $package;
- }
- return NULL;
- }
+ public function getPackageFileMappings(PackageInterface $package) : array {
+ $package_extra = $package->getExtra();
- /**
- * Get the command options.
- *
- * @param \Composer\Plugin\CommandEvent $event
- * The Composer event that called this listener.
- */
- public function onCmdBeginsEvent(CommandEvent $event) {
- if ($event->getInput()->hasOption('no-progress')) {
- $this->progress = !($event->getInput()->getOption('no-progress'));
+ if (isset($package_extra['composer-scaffold']['file-mapping'])) {
+ $package_file_mappings = $package_extra['composer-scaffold']['file-mapping'];
+ return $this->validatePackageFileMappings($package_file_mappings);
}
else {
- $this->progress = TRUE;
+ $this->io->writeError("The allowed package {$package->getName()} does not provide a file mapping for Composer Scaffold.");
+ return [];
}
}
/**
- * Event handler that fires after an install or update command.
+ * Validate the package file mappings.
*
- * Files to be processed are marked, and potentially problematic test files
- * are removed from vendor packages.
+ * Throw an exception if there are invalid values, and normalize the value otherwise.
*
- * @param \Composer\Installer\PackageEvent $event
- * The install or update event object from Composer.
+ * @param string[] $package_file_mappings
+ * An array of destination => source scaffold file mappings.
+ *
+ * @return string[]
+ * The provided $package_file_mappings with its array values normalized.
*/
- public function onPostPackageEvent(PackageEvent $event) {
- $package = $this->getCorePackage($event->getOperation());
- if ($package) {
- // By explicitly setting the core package, the onPostCmdEvent() will
- // process the scaffolding automatically.
- $this->drupalCorePackage = $package;
- }
-
- $vendor_dir = $event->getComposer()->getConfig()->get('vendor-dir');
- $io = $event->getIO();
+ protected function validatePackageFileMappings(array $package_file_mappings) {
+ $result = [];
- // Get target package if we're updating, package otherwise.
- $operation = $event->getOperation();
- if ($operation->getJobType() == 'update') {
- $package = $operation->getTargetPackage();
- }
- else {
- $package = $operation->getPackage();
+ foreach ($package_file_mappings as $key => $value) {
+ $result[$key] = $this->normalizeMapping($value);
}
- // Get the case-adjusted package name, which is also the path to the package
- // within the vendor directory.
- $package_name = static::findPackageKey($package->getName());
- if ($package_name) {
- $cleanup_paths = static::$packageToCleanup[$package_name];
- static::doTestCodeCleanup($vendor_dir, $package_name, $cleanup_paths, $io);
- }
+ return $result;
}
/**
- * Post install command event to execute the scaffolding.
+ * Normalize a value from the package file mappings.
*
- * @param \Composer\Script\Event $event
- * The Composer event.
+ * Currently, the valid values are:
+ * (bool) FALSE: Remove the scaffold file rather than scaffold it.
+ * 'relative/path': Path to the file to place, relative to the package root.
+ *
+ * In the future, we want to normalize to:
+ * [
+ * 'path' => 'relative/path',
+ * 'mode' => 'copy/prepend/append' // n.b. "copy" could make a symlink
+ * ]
+ *
+ * @param string|bool $value
+ * The value to normalize.
+ *
+ * @return string
+ * The normalized value.
*/
- public function onPostCmdEvent(Event $event) {
- // Only install the scaffolding if drupal/core was installed,
- // AND there are no scaffolding files present.
- if (isset($this->drupalCorePackage)) {
- $this->downloadScaffold();
- // Generate the autoload.php file after generating the scaffold files.
- $this->generateAutoload();
+ protected function normalizeMapping($value) {
+ if (is_bool($value)) {
+ if (!$value) {
+ return '';
+ }
+ throw new \Exception("File mapping $key cannot be given the value 'true'.");
+ }
+ if (empty($value)) {
+ throw new \Exception("File mapping $key cannot be an empty string.");
}
+ return $value;
}
/**
- * Downloads drupal scaffold files for the current process.
+ * Copies all scaffold files from source to destination.
*/
- public function downloadScaffold() {
- $drupal_core_package = $this->getDrupalCorePackage();
- $webroot = realpath($this->getWebRoot());
-
- // Collect options, excludes and settings files.
- $options = $this->getOptions();
- $files = array_diff($this->getIncludes(), $this->getExcludes());
-
+ public function scaffold() {
// Call any pre-scaffold scripts that may be defined.
$dispatcher = new EventDispatcher($this->composer, $this->io);
- $dispatcher->dispatch(self::PRE_DRUPAL_SCAFFOLD_CMD);
+ $dispatcher->dispatch(self::PRE_COMPOSER_SCAFFOLD_CMD);
+
+ $interpolator = $this->getLocationReplacements();
- $version = $this->getDrupalCoreVersion($drupal_core_package);
+ $this->allowedPackages = $this->getAllowedPackages();
+ $file_mappings = $this->getFileMappingsFromPackages($this->allowedPackages);
- $remoteFs = new RemoteFilesystem($this->io);
+ list($list_of_scaffold_files, $resolved_file_mappings) = $this->collateScaffoldFiles($file_mappings, $interpolator);
- $fetcher = new PrestissimoFileFetcher($remoteFs, $options['source'], $this->io, $this->composer->getConfig(), $this->progress);
- $fetcher->setFilenames(array_combine($files, $files));
- $fetcher->fetch($version, $webroot, TRUE);
+ $this->scaffoldPackageFiles($list_of_scaffold_files, $resolved_file_mappings);
- $fetcher->setFilenames($this->getInitial());
- $fetcher->fetch($version, $webroot, FALSE);
+ // Generate an autoload file in the document root that includes
+ // the autoload.php file in the vendor directory, wherever that is.
+ // Drupal requires this in order to easily locate relocated vendor dirs.
+ $this->generateAutoload();
// Call post-scaffold scripts.
- $dispatcher->dispatch(self::POST_DRUPAL_SCAFFOLD_CMD);
+ $dispatcher->dispatch(self::POST_COMPOSER_SCAFFOLD_CMD);
}
/**
- * Generate the autoload file at the Drupal root.
+ * Generate the autoload file at the project root.
*
* Include the autoload file that Composer generated.
*/
public function generateAutoload() {
- $vendor_path = $this->getVendorPath();
+ $vendorPath = $this->getVendorPath();
$webroot = $this->getWebRoot();
- // Calculate the relative path from the webroot (location of the
- // project autoload.php) to the vendor directory.
+ // Calculate the relative path from the webroot (location of the project
+ // autoload.php) to the vendor directory.
$fs = new SymfonyFilesystem();
- $relative_vendor_path = $fs->makePathRelative($vendor_path, realpath($webroot));
+ $relativeVendorPath = $fs->makePathRelative($vendorPath, realpath($webroot));
- $fs->dumpFile($webroot . "/autoload.php", $this->autoLoadContents($relative_vendor_path));
+ $fs->dumpFile($webroot . "/autoload.php", $this->autoLoadContents($relativeVendorPath));
}
/**
- * Build the contents of the docroot autoload file.
- *
- * This allows the vendor directory to live outside of the docroot.
- *
- * @param string $relativeVendorPath
- * Path to the vendor directory, relative to the Drupal root.
+ * Build the contents of the autoload file.
*
* @return string
- * The contents of the autoload.php for the docroot directory.
+ * Return the contents for the autoload.php.
*/
- protected function autoLoadContents($relativeVendorPath) {
+ protected function autoLoadContents(string $relativeVendorPath) : string {
$relativeVendorPath = rtrim($relativeVendorPath, '/');
- $autoload_contents = <<composer->getConfig();
+ $vendorDir = $this->composer->getConfig()->get('vendor-dir');
$filesystem = new Filesystem();
- $filesystem->ensureDirectoryExists($config->get('vendor-dir'));
- $vendor_path = $filesystem->normalizePath(realpath($config->get('vendor-dir')));
-
- return $vendor_path;
+ $filesystem->ensureDirectoryExists($vendorDir);
+ return $filesystem->normalizePath(realpath($vendorDir));
}
/**
- * Returns the drupal core package object.
+ * Retrieve the path to the web root.
*
- * Look up the Drupal core package object, or return it from where we cached
- * it in the $drupalCorePackage field.
+ * @return string
+ * The file path of the web root.
*
- * @return \Composer\Package\PackageInterface
- * Composer package for drupal/core.
+ * @throws \Exception
*/
- public function getDrupalCorePackage() {
- if (!isset($this->drupalCorePackage)) {
- $this->drupalCorePackage = $this->getPackage('drupal/core');
+ public function getWebRoot() {
+ $options = $this->getOptions();
+ // @todo Allow packages to set web root location?
+ if (empty($options['locations']['web-root'])) {
+ throw new \Exception("The extra.composer-scaffold.location.web-root is not set in composer.json.");
}
- return $this->drupalCorePackage;
+ return $options['locations']['web-root'];
}
/**
- * Returns the Drupal core version for the given package.
+ * Retrieve a package from the current composer process.
*
- * @param \Composer\Package\PackageInterface $drupalCorePackage
- * The Composer package for drupal/core.
+ * @param string $name
+ * Name of the package to get from the current composer installation.
*
- * @return string
- * The version number of drupal/core in this installation.
+ * @return \Composer\Package\PackageInterface|null
+ * The Composer package.
*/
- protected function getDrupalCoreVersion(PackageInterface $drupalCorePackage) {
- $version = $drupalCorePackage->getPrettyVersion();
- if ($drupalCorePackage->getStability() == 'dev' && substr($version, -4) == '-dev') {
- $version = substr($version, 0, -4);
- return $version;
+ protected function getPackage(string $name) {
+ $package = $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
+ if (is_null($package)) {
+ $this->io->write("Composer Scaffold could not find installed package `$name`.");
}
- return $version;
+
+ return $package;
}
/**
- * Retrieve the path to the web root.
+ * Retrieve options from optional "extra" configuration.
*
- * @return string
- * The webroot. Well, actually, the drupal root.
+ * @return array
+ * The composer-scaffold configuration array.
*/
- public function getWebRoot() {
- $drupal_core_package = $this->getDrupalCorePackage();
- $installation_manager = $this->composer->getInstallationManager();
- $core_path = $installation_manager->getInstallPath($drupal_core_package);
- // Webroot is the parent path of the drupal core installation path.
- $webroot = dirname($core_path);
+ protected function getOptions() : array {
+ $extra = $this->composer->getPackage()->getExtra() + ['composer-scaffold' => []];
- return $webroot;
+ return $extra['composer-scaffold'] + [
+ "allowed-packages" => [],
+ "locations" => [],
+ "symlink" => FALSE,
+ "file-mapping" => [],
+ ];
}
/**
- * Retrieve a package from the current composer process.
+ * Merges arrays recursively while preserving.
*
- * @param string $name
- * Name of the package to get from the current composer installation.
+ * @param array $array1
+ * The first array.
+ * @param array $array2
+ * The second array.
*
- * @return \Composer\Package\PackageInterface
- * A Composer Package matching the given name.
- */
- protected function getPackage($name) {
- return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
- }
-
- /**
- * Retrieve excludes from optional "extra" configuration.
+ * @return array
+ * The merged array.
*
- * @return string[]
- * Any scaffold files that we would like to exclude from the default config.
+ * @see http://php.net/manual/en/function.array-merge-recursive.php#92195
+ * @todo: Remove
*/
- protected function getExcludes() {
- return $this->getNamedOptionList('excludes', 'getExcludesDefault');
+ public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2) : array {
+ $merged = $array1;
+ foreach ($array2 as $key => &$value) {
+ if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
+ $merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
+ }
+ else {
+ $merged[$key] = $value;
+ }
+ }
+ return $merged;
}
/**
- * Retrieve list of additional files from optional "extra" configuration.
+ * GetLocationReplacements creates an interpolator for the 'locations' element.
*
- * @return string[]
- * Additional scaffold files we want to retrieve.
- */
- protected function getIncludes() {
- return $this->getNamedOptionList('includes', 'getIncludesDefault');
- }
-
- /**
- * Retrieve list of initial files from optional "extra" configuration.
+ * The interpolator returned will replace a path string with the tokens
+ * defined in the 'locations' element.
*
- * @return string[]
- * A list of files that should only be scaffolded on primary installation.
+ * @return Interpolator
+ * Object that will do replacements in a string using tokens in 'locations' element.
*/
- protected function getInitial() {
- return $this->getNamedOptionList('initial', 'getInitialDefault');
+ public function getLocationReplacements() {
+ $interpolator = new Interpolator();
+
+ $fs = new Filesystem();
+ $options = $this->getOptions();
+ $locations = $options['locations'] + ['web_root' => './'];
+ $locations = array_map(
+ function ($location) use ($fs) {
+ $fs->ensureDirectoryExists($location);
+ $location = realpath($location);
+ return $location;
+ },
+ $locations
+ );
+
+ return $interpolator->setData($locations);
}
/**
- * Gets a configuration value from extra config for drupal-scaffold.
+ * Copy all files, as defined by $file_mappings.
*
- * Retrieve a named list of options from optional "extra" configuration.
- * Respects 'omit-defaults', and either includes or does not include the
- * default values, as requested.
+ * @param array $file_mappings
+ * An multidimensional array of file mappings, as returned by
+ * self::getFileMappingsFromPackages().
+ * @param Interpolator $interpolator
+ * An object with the location mappings (e.g. [web-root]).
*
- * @param string $optionName
- * The particular option value to retrieve.
- * @param string $defaultFn
- * Function to gather defaults from for this option.
- *
- * @return string[]
- * Array of defined values in the extra config.
+ * @return array
+ * - $list_of_scaffold_files: a list of all of the files to scaffold
+ * - $resolved_file_mappings original $file_mappings by package, with
+ * values converted to ScaffoldFileInfo objects
*/
- protected function getNamedOptionList($optionName, $defaultFn) {
- $options = $this->getOptions();
- $result = [];
- if (empty($options['omit-defaults'])) {
- $result = $this->$defaultFn();
+ protected function collateScaffoldFiles(array $file_mappings, Interpolator $interpolator) {
+ $resolved_file_mappings = [];
+ $resolved_package_file_list = [];
+ foreach ($file_mappings as $package_name => $package_file_mappings) {
+ if (!$this->getAllowedPackage($package_name)) {
+ // It should no longer be possible to get here.
+ $this->io->writeError("The package $package_name is listed in file-mappings, but not an allowed package. Skipping.");
+ continue;
+ }
+ $package_path = $this->getPackagePath($package_name);
+ foreach ($package_file_mappings as $destination_rel_path => $source_rel_path) {
+ $src_full_path = $this->resolveSourceLocation($package_name, $package_path, $source_rel_path);
+ $dest_full_path = $interpolator->interpolate($destination_rel_path);
+
+ $scaffold_file = (new ScaffoldFileInfo())
+ ->setPackageName($package_name)
+ ->setDestinationRelativePath($destination_rel_path)
+ ->setSourceRelativePath($source_rel_path)
+ ->setDestinationFullPath($dest_full_path)
+ ->setSourceFullPath($src_full_path);
+
+ $list_of_scaffold_files[$destination_rel_path] = $scaffold_file;
+ $resolved_file_mappings[$package_name][$destination_rel_path] = $scaffold_file;
+ }
}
- $result = array_merge($result, (array) $options[$optionName]);
-
- return $result;
+ return [$list_of_scaffold_files, $resolved_file_mappings];
}
/**
- * Retrieve excludes from optional "extra" configuration.
+ * Gets an allowed package from $this->allowedPackages array.
*
- * The 'source' option is the url pattern where we locate the files, e.g.
- * Github: https://raw.githubusercontent.com/drupal/drupal/{version}/{path}
- * Drupal.org: https://cgit.drupalcode.org/drupal/plain/{path}?h={version}
+ * @param string $package_name
+ * The Composer package name. E.g., drupal/core.
*
- * @return string[]
- * An array of plugin configuration options.
+ * @return \Composer\Package\Package|null
+ * The allowed Composer package, if it exists.
*/
- protected function getOptions() {
- $extra = $this->composer->getPackage()->getExtra() + ['drupal-scaffold' => []];
- $options = $extra['drupal-scaffold'] + [
- 'omit-defaults' => FALSE,
- 'excludes' => [],
- 'includes' => [],
- 'initial' => [],
- 'source' => 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}',
- ];
- return $options;
+ protected function getAllowedPackage($package_name) {
+ if (array_key_exists($package_name, $this->allowedPackages)) {
+ return $this->allowedPackages[$package_name];
+ }
+
+ return NULL;
}
/**
- * Holds default excludes.
- *
- * @return string[]
- * Nothing is excluded by default.
- */
- protected function getExcludesDefault() {
- return [];
+ * Gets a consolidated list of file mappings from all allowed packages.
+ *
+ * @param \Composer\Package\Package[] $allowed_packages
+ * A multidimensional array of file mappings, as returned by
+ * self::getAllowedPackages().
+ *
+ * @return array
+ * An multidimensional array of file mappings, which looks like this:
+ * [
+ * 'drupal/core' => [
+ * 'path/to/source/file' => 'path/to/destination',
+ * 'path/to/source/file' => false,
+ * ],
+ * 'some/package' => [
+ * 'path/to/source/file' => 'path/to/destination',
+ * ],
+ * ]
+ */
+ protected function getFileMappingsFromPackages(array $allowed_packages) : array {
+ $file_mappings = [];
+ foreach ($allowed_packages as $name => $package) {
+ $package_file_mappings = $this->getPackageFileMappings($package);
+ $file_mappings[$name] = $package_file_mappings;
+ }
+ return $file_mappings;
}
/**
- * Holds default settings files list.
+ * Gets a list of all packages that are allowed to copy scaffold files.
*
- * @return string[]
- * The default scaffolding files that come with drupal.
+ * Configuration for packages specified later will override configuration
+ * specified by packages listed earlier. In other words, the last listed
+ * package has the highest priority. The root package will always be returned
+ * at the end of the list.
+ *
+ * @return \Composer\Package\PackageInterface[]
+ * An array of allowed Composer packages.
*/
- protected function getIncludesDefault() {
-
- $common = [
- '.csslintrc',
- '.editorconfig',
- '.eslintignore',
- '.eslintrc.json',
- '.gitattributes',
- '.ht.router.php',
- '.htaccess',
- 'autoload.php',
- 'example.gitignore',
- 'index.php',
- 'INSTALL.txt',
- 'README.txt',
- 'robots.txt',
- 'update.php',
- 'web.config',
- 'modules/README.txt',
- 'profiles/README.txt',
- 'sites/default/default.settings.php',
- 'sites/default/default.services.yml',
- 'sites/development.services.yml',
- 'sites/example.settings.local.php',
- 'sites/example.sites.php',
- 'sites/README.txt',
- 'themes/README.txt',
+ protected function getAllowedPackages(): array {
+ $root_package = $this->composer->getPackage();
+ $options = $this->getOptions() + [
+ 'allowed-packages' => [],
+ 'file-mapping' => [],
];
+ $allowed_packages = [];
+ foreach ($options['allowed-packages'] as $name) {
+ if ($root_package->getName() === $name) {
+ continue;
+ }
+ $package = $this->getPackage($name);
+ if ($package instanceof PackageInterface) {
+ $allowed_packages[$name] = $package;
+ }
+ }
+
+ // Add root package at the end so that it overrides all the preceding
+ // package.
+ $allowed_packages[$root_package->getName()] = $root_package;
- sort($common);
- return $common;
+ return $allowed_packages;
}
/**
- * Holds default initial files.
+ * Gets the file path of a package.
*
- * @return string[]
- * Empty array for no initial default.
- */
- protected function getInitialDefault() {
- return [];
+ * @param string $package_name
+ * The package name.
+ *
+ * @return string
+ * The file path.
+ */
+ protected function getPackagePath(string $package_name) : string {
+ if ($package_name == $this->composer->getPackage()->getName()) {
+ // This will respect the --working-dir option if Composer is invoked with
+ // it. There is no API or method to determine the filesystem path of
+ // a package's composer.json file.
+ return getcwd();
+ }
+ else {
+ return $this->composer->getInstallationManager()->getInstallPath($this->getPackage($package_name));
+ }
}
/**
- * Remove possibly problematic test files from a single vendor package.
+ * ResolveSourceLocation converts the relative source path into an absolute path.
*
- * @param string $vendor_dir
- * Full path to the vendor directory.
+ * The path returned will be relative to the package installation location.
+ *
+ * @param string $package_name
+ * Name of the package containing the source file.
* @param string $package_path
- * The package path within the vendor directory. Should also happen to be
- * the package name. Example: psr/log.
- * @param string[] $cleanup_paths
- * An array of relative paths within the vendor path which should be
- * removed.
- * @param \Composer\IO\IOInterface $io
- * IO object provided by Composer.
+ * Path to the root of the named package.
+ * @param string $source
+ * Source location provided as a relative path.
+ *
+ * @return string
+ * Source location converted to an absolute path, or empty if removed.
*/
- protected static function doTestCodeCleanup($vendor_dir, $package_path, array $cleanup_paths, IOInterface $io) {
- $package_dir = $vendor_dir . '/' . $package_path;
- if (is_dir($package_dir)) {
- $io->write(sprintf(" Test code cleanup for %s", $package_path), TRUE, $io::VERY_VERBOSE);
- foreach ($cleanup_paths as $cleanup_path) {
- $cleanup_dir = $package_dir . '/' . $cleanup_path;
- if (is_dir($cleanup_dir)) {
- // Try to clean up.
- if (static::deleteRecursive($cleanup_dir)) {
- $io->write(sprintf(" Removing directory '%s'", $cleanup_path), TRUE, $io::VERY_VERBOSE);
- }
- else {
- // Always display a message if this fails as it means something
- // has gone wrong. Therefore the message has to include the
- // package name as the first informational message might not
- // exist.
- $io->write(sprintf(" Failure removing directory '%s' in package %s.", $cleanup_path, $package_path), TRUE, IOInterface::NORMAL);
- }
- }
- else {
- // If the package has changed or the --prefer-dist version does not
- // include the directory this is not an error.
- $io->write(sprintf(" Directory '%s' does not exist", $cleanup_dir), TRUE, $io::VERY_VERBOSE);
- }
- }
- $io->write('', TRUE, $io::VERY_VERBOSE);
+ public function resolveSourceLocation(string $package_name, string $package_path, string $source) {
+ if (empty($source)) {
+ return '';
+ }
+
+ $source_path = $package_path . '/' . $source;
+
+ if (!file_exists($source_path)) {
+ throw new \Exception("Could not find source file $source_path for package $package_name\n");
}
+ if (is_dir($source_path)) {
+ throw new \Exception("$source_path in $package_name is a directory; only files may be scaffolded.");
+ }
+
+ return $source_path;
}
/**
- * Find the array key for a given package name with a case-insensitive search.
+ * Scaffolds the files for a specific package.
*
- * @param string $package_name
- * The package name from composer. This is always already lower case.
- *
- * @return string|null
- * The string key, or NULL if none was found.
+ * @param array $list_of_scaffold_files
+ * An associative array of destination file mappings.
+ * @param array $resolved_file_mappings
+ * An associative array of package name to [dest => scaffold info mappings].
*/
- protected static function findPackageKey($package_name) {
- $package_key = NULL;
- // In most cases the package name is already used as the array key.
- if (isset(static::$packageToCleanup[$package_name])) {
- $package_key = $package_name;
- }
- else {
- // Handle any mismatch in case between the package name and array key.
- // For example, the array key 'mikey179/vfsStream' needs to be found
- // when composer returns a package name of 'mikey179/vfsstream'.
- foreach (static::$packageToCleanup as $key => $dirs) {
- if (strtolower($key) === $package_name) {
- $package_key = $key;
- break;
+ protected function scaffoldPackageFiles(array $list_of_scaffold_files, array $resolved_file_mappings) {
+ $options = $this->getOptions();
+ $symlink = $options['symlink'];
+
+ // We could simply scaffold all of the files from $list_of_scaffold_files,
+ // which contain only the list of files to be processed. We iterate over
+ // $resolved_file_mappings instead so that we can print out all of the
+ // scaffold files grouped by the package that provided them, including
+ // those not being scaffolded (because they were overridden or removed
+ // by some later package).
+ foreach ($resolved_file_mappings as $package_name => $package_scaffold_files) {
+ $this->io->write("Scaffolding files for $package_name:");
+ foreach ($package_scaffold_files as $dest_rel_path => $scaffold_file) {
+ $overriding_package = $scaffold_file->findProvidingPackage($list_of_scaffold_files);
+ if ($scaffold_file->overridden($overriding_package)) {
+ $this->io->write($scaffold_file->interpolate(" - [dest-rel-path] overridden in $overriding_package"));
+ }
+ else {
+ $this->process($scaffold_file, $symlink);
}
}
}
- return $package_key;
}
/**
- * Helper method to remove directories and the files they contain.
+ * Moves a single scaffold file from source to destination.
*
- * @param string $path
- * The directory or file to remove. It must exist.
+ * @param ScaffoldFileInfo $scaffold_file
+ * The scaffold file to be processed.
+ * @param bool $symlink
+ * Whether the destination should be a symlink.
*
- * @return bool
- * TRUE on success or FALSE on failure.
+ * @throws \Exception
*/
- protected static function deleteRecursive($path) {
- if (is_file($path) || is_link($path)) {
- return unlink($path);
+ protected function process(ScaffoldFileInfo $scaffold_file, bool $symlink) {
+ $fs = new Filesystem();
+
+ if ($scaffold_file->removed()) {
+ return;
}
- $success = TRUE;
- $dir = dir($path);
- while (($entry = $dir->read()) !== FALSE) {
- if ($entry == '.' || $entry == '..') {
- continue;
+
+ $destination_path = $scaffold_file->getDestinationFullPath();
+ $source_path = $scaffold_file->getSourceFullPath();
+
+ // Get rid of the destination if it exists, and make sure that
+ // the directory where it's going to be placed exists.
+ @unlink($destination_path);
+ $fs->ensureDirectoryExists(dirname($destination_path));
+ $success = FALSE;
+ if ($symlink) {
+ try {
+ $success = $fs->relativeSymlink($source_path, $destination_path);
+ }
+ catch (\Exception $e) {
}
- $entry_path = $path . '/' . $entry;
- $success = static::deleteRecursive($entry_path) && $success;
}
- $dir->close();
-
- return rmdir($path) && $success;
+ else {
+ $success = copy($source_path, $destination_path);
+ }
+ $verb = $symlink ? 'symlink' : 'copy';
+ if (!$success) {
+ throw new \Exception($scaffold_file->interpolate("Could not $verb source file [src-rel-path] to [dest-rel-path]!"));
+ }
+ else {
+ $this->io->write($scaffold_file->interpolate(" - $verb source file [src-rel-path] to [dest-rel-path]"));
+ }
}
}
diff --git a/core/lib/Drupal/Component/Scaffold/Interpolator.php b/core/lib/Drupal/Component/Scaffold/Interpolator.php
new file mode 100644
index 0000000000..02b70e1842
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Interpolator.php
@@ -0,0 +1,144 @@
+startToken = $startToken;
+ $this->endToken = $endToken;
+ $this->data = [];
+ }
+
+ /**
+ * GetData fetches the data set used by this interpolator.
+ */
+ public function getData() {
+ return $this->data;
+ }
+
+ /**
+ * SetData allows the client to associate a standard data set to use when interpolating.
+ */
+ public function setData($data) {
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Interpolate a message using the standard data set provided via self::setData().
+ */
+ public function interpolate($message, $default = '') {
+ return $this->interpolateData($this->data, $message, $default = '');
+ }
+
+ /**
+ * Interpolate replaces tokens in a string with values from an associative array.
+ *
+ * Tokens are surrounded by double curley braces, e.g. "[key]". The characters
+ * that surround the key may be defined when the Interpolator is constructed.
+ *
+ * Example:
+ * If the message is 'Hello, [user.name]', then the value of the user.name
+ * item is fetched from the array, and the token [user.name] is
+ * replaced with the result.
+ *
+ * @param mixed|array $data
+ * Data to use for interpolation.
+ * @param string $message
+ * Message containing tokens to be replaced.
+ * @param string|bool $default
+ * The value to substitute for tokens that
+ * are not found in the data. If `false`, then missing
+ * tokens are not replaced.
+ *
+ * @return string
+ * The message after replacements have been made.
+ */
+ public function interpolateData($data, $message, $default = '') {
+ $replacements = $this->replacements($data, $message, $default);
+ return strtr($message, $replacements);
+ }
+
+ /**
+ * Throw if any tokens remain after interpolation.
+ */
+ public function mustInterpolate($data, $message) {
+ $result = $this->interpolate($data, $message, FALSE);
+ $tokens = $this->findTokens($result);
+ if (!empty($tokens)) {
+ throw new \Exception('The following required keys were not found in configuration: ' . implode(',', $tokens));
+ }
+ return $result;
+ }
+
+ /**
+ * FindTokens finds all of the tokens in the provided message.
+ *
+ * @param string $message
+ * String with tokens.
+ *
+ * @return string[]
+ * map of token to key, e.g. {{key}} => key
+ */
+ public function findTokens($message) {
+ $regEx = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#';
+
+ if (!preg_match_all($regEx, $message, $matches, PREG_SET_ORDER)) {
+ return [];
+ }
+
+ $tokens = [];
+ foreach ($matches as $matchSet) {
+ list($sourceText, $key) = $matchSet;
+ $tokens[$sourceText] = $key;
+ }
+ return $tokens;
+ }
+
+ /**
+ * Replacements finds the tokens that exist in a message and builds a replacement array.
+ *
+ * All of the replacements in the data array are looked up given the token
+ * keys from the provided message. Keys that do not exist in the configuration
+ * are replaced with the default value.
+ */
+ public function replacements($data, $message, $default = '') {
+ $tokens = $this->findTokens($message);
+
+ $replacements = [];
+ foreach ($tokens as $sourceText => $key) {
+ $replacementText = $this->get($data, $key, $default);
+ if ($replacementText !== FALSE) {
+ $replacements[$sourceText] = $replacementText;
+ }
+ }
+ return $replacements;
+ }
+
+ /**
+ * Get a value from an array. Throw if the type is wrong.
+ */
+ protected function get($data, $key, $default) {
+ if (is_array($data)) {
+ return array_key_exists($key, $data) ? $data[$key] : $default;
+ }
+ throw new \Exception('Bad data type provided to Interpolator');
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Plugin.php b/core/lib/Drupal/Component/Scaffold/Plugin.php
index 81f27d8ea2..e394bc88c3 100644
--- a/core/lib/Drupal/Component/Scaffold/Plugin.php
+++ b/core/lib/Drupal/Component/Scaffold/Plugin.php
@@ -3,16 +3,13 @@
namespace Drupal\Component\Scaffold;
use Composer\Script\Event;
-use Composer\Plugin\CommandEvent;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
-use Composer\Installer\PackageEvent;
-use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Plugin\Capable;
-use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Script\ScriptEvents;
+use Drupal\Component\Scaffold\CommandProvider;
/**
* Composer plugin for handling drupal scaffold.
@@ -20,7 +17,7 @@
class Plugin implements PluginInterface, EventSubscriberInterface, Capable {
/**
- * Handler class that does the actual processing of the scaffolding.
+ * The Composer Scaffold handler.
*
* @var \Drupal\Component\Scaffold\Handler
*/
@@ -42,7 +39,7 @@ public function activate(Composer $composer, IOInterface $io) {
*/
public function getCapabilities() {
return [
- 'Composer\Plugin\Capability\CommandProvider' => 'Drupal\Component\Scaffold\CommandProvider',
+ 'Composer\Plugin\Capability\CommandProvider' => CommandProvider::class,
];
}
@@ -51,39 +48,15 @@ public function getCapabilities() {
*/
public static function getSubscribedEvents() {
return [
- PackageEvents::POST_PACKAGE_INSTALL => 'postPackage',
- PackageEvents::POST_PACKAGE_UPDATE => 'postPackage',
ScriptEvents::POST_UPDATE_CMD => 'postCmd',
- ScriptEvents::POST_CREATE_PROJECT_CMD => 'postCmd',
- PluginEvents::COMMAND => 'cmdBegins',
];
}
- /**
- * Command begins event callback.
- *
- * @param \Composer\Plugin\CommandEvent $event
- * Composer event sent on command execution.
- */
- public function cmdBegins(CommandEvent $event) {
- $this->handler->onCmdBeginsEvent($event);
- }
-
- /**
- * Post package event behaviour.
- *
- * @param \Composer\Installer\PackageEvent $event
- * Composer package event sent on install/update/remove.
- */
- public function postPackage(PackageEvent $event) {
- $this->handler->onPostPackageEvent($event);
- }
-
/**
* Post command event callback.
*
* @param \Composer\Script\Event $event
- * Composer script event sent when commands are run.
+ * The Composer event.
*/
public function postCmd(Event $event) {
$this->handler->onPostCmdEvent($event);
diff --git a/core/lib/Drupal/Component/Scaffold/PrestissimoFileFetcher.php b/core/lib/Drupal/Component/Scaffold/PrestissimoFileFetcher.php
deleted file mode 100644
index d6cb7bb96c..0000000000
--- a/core/lib/Drupal/Component/Scaffold/PrestissimoFileFetcher.php
+++ /dev/null
@@ -1,100 +0,0 @@
-config = $config;
- }
-
- /**
- * {@inheritdoc}
- */
- public function fetch($version, $destination, $override) {
- if (class_exists(CurlMulti::class)) {
- $this->fetchWithPrestissimo($version, $destination, $override);
- return;
- }
- parent::fetch($version, $destination, $override);
- }
-
- /**
- * Fetch files in parallel.
- *
- * @param string $version
- * The version of the scaffold file to be retrieved.
- * @param string $destination
- * The location on the filesystem where we will place the file.
- * @param bool $override
- * Whether the file should be overridden or left in place.
- */
- protected function fetchWithPrestissimo($version, $destination, $override) {
- $requests = [];
-
- foreach ($this->filenames as $sourceFilename => $filename) {
- $target = "$destination/$filename";
- if ($override || !file_exists($target)) {
- $url = $this->getUri($sourceFilename, $version);
- $this->fs->ensureDirectoryExists($destination . '/' . dirname($filename));
- $requests[] = new CopyRequest($url, $target, FALSE, $this->io, $this->config);
- }
- }
-
- $successCnt = $failureCnt = 0;
- $totalCnt = count($requests);
- if ($totalCnt == 0) {
- return;
- }
-
- $multi = new CurlMulti();
- $multi->setRequests($requests);
- do {
- $multi->setupEventLoop();
- $multi->wait();
- $result = $multi->getFinishedResults();
- $successCnt += $result['successCnt'];
- $failureCnt += $result['failureCnt'];
- if ($this->progress) {
- foreach ($result['urls'] as $url) {
- $this->io->writeError(" - Downloading $successCnt/$totalCnt: $url", TRUE);
- }
- }
- } while ($multi->remain());
- }
-
-}
diff --git a/core/lib/Drupal/Component/Scaffold/README.md b/core/lib/Drupal/Component/Scaffold/README.md
index 3ead55067f..59723fc6a6 100644
--- a/core/lib/Drupal/Component/Scaffold/README.md
+++ b/core/lib/Drupal/Component/Scaffold/README.md
@@ -1,126 +1,134 @@
-# drupal-scaffold
+The Drupal Composer Scaffold Plugin
-Composer plugin for automatically downloading Drupal scaffold files (like
-`index.php`, `update.php`, etc) when requiring `drupal/core` via Composer.
+Thanks for using this Drupal component.
-It is recommended that the vendor directory be placed in its standard location
-at the project root, outside of the Drupal root; however, the location of the
-vendor directory and the Drupal root may be placed in whatever
-location suits the project. Drupal-scaffold will generate the autoload.php
-file at the Drupal root to require the Composer-generated autoload file in the
-vendor directory.
+You can participate in its development on Drupal.org, through our issue system:
+https://www.drupal.org/project/issues/drupal
-## Usage
+You can get the full Drupal repo here:
+https://www.drupal.org/project/drupal/git-instructions
-Run `composer require drupal/drupal-scaffold` in your Composer
-project before installing or updating `drupal/core`.
+You can browse the full Drupal repo here:
+http://cgit.drupalcode.org/drupal
-Once drupal-scaffold is required by your project, it will automatically update
-your scaffold files whenever `composer update` changes the version of
-`drupal/core` installed.
+Sample composer.json for a project that relies on packages that use composer-scaffold:
+```
+{
+ "name": "my/project",
+ "require": {
+ "drupal/composer-scaffold": "*",
+ "composer/installers": "^1.2",
+ "cweagans/composer-patches": "^1.6.5",
+ "drupal/core": "^8.8.x-dev",
+ "pantheon-systems/d8-scaffold-files": "^1"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "sort-packages": true
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "drupal/core",
+ ],
+ "locations": {
+ "web-root": "./docroot"
+ },
+ "symlink": true,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt"
+ }
+ }
+ }
+}
+```
-## Configuration
+Sample composer.json for composer-scaffold files in drupal/core:
-You can configure the plugin by providing some settings in the `extra` section
-of your root `composer.json`.
+```
+{
+ "name": "drupal/core",
+ "extra": {
+ "composer-scaffold": {
+ "file-mapping": {
+ "[web-root]/.csslintrc": "assets/.csslintrc",
+ "[web-root]/.editorconfig": "assets/.editorconfig",
+ "[web-root]/.eslintignore": "assets/.eslintignore",
+ "[web-root]/.eslintrc.json": "assets/.eslintrc.json",
+ "[web-root]/.gitattributes": "assets/.gitattributes",
+ "[web-root]/.ht.router.php": "assets/.ht.router.php",
+ "[web-root]/.htaccess": "assets/.htaccess",
+ "[web-root]/sites/default/default.services.yml": "assets/default.services.yml",
+ "[web-root]/sites/default/default.settings.php": "assets/default.settings.php",
+ "[web-root]/sites/example.settings.local.php": "assets/example.settings.local.php",
+ "[web-root]/sites/example.sites.php": "assets/example.sites.php",
+ "[web-root]/index.php": "assets/index.php",
+ "[web-root]/robots.txt": "assets/robots.txt",
+ "[web-root]/update.php": "assets/update.php",
+ "[web-root]/web.config": "assets/web.config"
+ }
+ }
+ }
+}
+```
-```json
+@todo: drupal/core can move assets to a different project:
+
+```
{
+ "name": "drupal/core",
"extra": {
- "drupal-scaffold": {
- "source": "https://cgit.drupalcode.org/drupal/plain/{path}?h={version}",
- "excludes": [
- "google123.html",
- "robots.txt"
- ],
- "includes": [
- "sites/default/example.settings.my.php"
- ],
- "initial": {
- "sites/default/default.services.yml": "sites/default/services.yml",
- "sites/default/default.settings.php": "sites/default/settings.php"
- },
- "omit-defaults": false
+ "composer-scaffold": {
+ "allowed-packages": [
+ "drupal/assets",
+ ]
}
}
}
```
-The `source` option may be used to specify the URL to download the
-scaffold files from; the default source is cgit.drupalcode.org. The literal string
-`{version}` in the `source` option is replaced with the current version of
-Drupal core being updated prior to download.
-With the `drupal-scaffold` option `excludes`, you can provide additional paths
-that should not be copied or overwritten. The plugin provides no excludes by
-default.
+Sample composer.json for a library that implements composer-scaffold:
-Default includes are provided by the plugin:
```
-.csslintrc
-.editorconfig
-.eslintignore
-.eslintrc.json
-.gitattributes
-.ht.router.php
-.htaccess
-example.gitignore
-index.php
-INSTALL.txt
-README.txt
-robots.txt
-modules/README.txt
-profiles/README.txt
-sites/README.txt
-sites/default/default.settings.php
-sites/default/default.services.yml
-sites/development.services.yml
-sites/example.settings.local.php
-sites/example.sites.php
-themes/README.txt
-update.php
-web.config
+{
+ "name": "pantheon-systems/d8-scaffold-files",
+ "extra": {
+ "composer-scaffold": {
+ "file-mapping": {
+ "[web-root]/sites/default/settings.php": "assets/sites/default/settings.php"
+ }
+ }
+ }
+ }
+}
```
-When setting `omit-defaults` to `true`, neither the default excludes nor the
-default includes will be provided; in this instance, only those files explicitly
-listed in the `excludes` and `includes` options will be considered. If
-`omit-defaults` is `false` (the default), then any items listed in `excludes`
-or `includes` will be in addition to the usual defaults.
-
-The `initial` hash lists files that should be copied over only if they do not
-exist in the destination. The key specifies the path to the source file, and
-the value indicates the path to the destination file.
-
-## Limitation
-
-When using Composer to install or update the Drupal development branch, the
-scaffold files are always taken from the HEAD of the branch (or, more
-specifically, from the most recent development .tar.gz archive). This might
-not be what you want when using an old development version (e.g. when the
-version is fixed via composer.lock). To avoid problems, always commit your
-scaffold files to the repository any time that composer.lock is committed.
-Note that the correct scaffold files are retrieved when using a tagged release
-of `drupal/core` (recommended).
-
-## Custom command
-
-The plugin by default is only downloading the scaffold files when installing or
-updating `drupal/core`. You can call it manually with "composer drupal:scaffold"
-or "@composer drupal:scaffold" in Composer Scripts.
-
-```json
-"scripts": {
- "post-install-cmd": [
- "@composer drupal:scaffold",
- "..."
- ],
- "post-update-cmd": [
- "@composer drupal:scaffold",
- "..."
- ]
-},
+@todo: Append to robots.txt:
+
```
+{
+ "name": "pantheon-systems/d8-scaffold-files",
+ "extra": {
+ "composer-scaffold": {
+ "file-mapping": {
+ "[web-root]/robots.txt": {
+ "path": "assets/my-robots-additions.txt",
+ "mode": "append"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+With the enhancement above, `false` will expand to `"mode": "remove"`, and a simple string will expand to `"mode": "copy"`, with the string's value being placed in "path". This will therefore retain backwards compatibility with the current implementation, and also will maintain brief, descriptive file mappings for the most common cases (copy and remove).
-It is assumed that the scaffold files will be committed to the repository, to
-ensure that the correct files are used on the CI server (see **Limitation**,
-above). Commit the scaffold files to your repository after running `composer install` for the first time.
+Patch a file after it's copied:
+
+```
+"post-composer-scaffold-cmd": [
+ "cd docroot && patch -p1 <../patches/htaccess-ssl.patch"
+]
+```
\ No newline at end of file
diff --git a/core/lib/Drupal/Component/Scaffold/ScaffoldFileInfo.php b/core/lib/Drupal/Component/Scaffold/ScaffoldFileInfo.php
new file mode 100644
index 0000000000..27df81578f
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/ScaffoldFileInfo.php
@@ -0,0 +1,198 @@
+packageName = $packageName;
+ return $this;
+ }
+
+ /**
+ * Get the package name.
+ *
+ * @return string
+ * The name of the package this scaffold file info was collected from.
+ */
+ public function getPackageName() {
+ return $this->packageName;
+ }
+
+ /**
+ * Set the relative path to the destination.
+ *
+ * @param string $destinationRelPath
+ * The relative path to the destination file.
+ *
+ * @return $this
+ */
+ public function setDestinationRelativePath(string $destinationRelPath) {
+ $this->destinationRelPath = $destinationRelPath;
+ return $this;
+ }
+
+ /**
+ * Get the relative path to the destination.
+ *
+ * @return string
+ * The relative path to the destination file.
+ */
+ public function getDestinationRelativePath() {
+ return $this->destinationRelPath;
+ }
+
+ /**
+ * Set the relative path to the source.
+ *
+ * @param string $sourceRelPath
+ * The relative path to the source file.
+ *
+ * @return $this
+ */
+ public function setSourceRelativePath(string $sourceRelPath) {
+ $this->sourceRelPath = $sourceRelPath;
+ return $this;
+ }
+
+ /**
+ * Get the relative path to the source.
+ *
+ * @return string
+ * The relative path to the source file.
+ */
+ public function getSourceRelativePath() {
+ return $this->sourceRelPath;
+ }
+
+ /**
+ * Set the full path to the destination.
+ *
+ * @param string $destinationFullPath
+ * The full path to the destination file.
+ *
+ * @return $this
+ */
+ public function setDestinationFullPath(string $destinationFullPath) {
+ $this->destinationFullPath = $destinationFullPath;
+ return $this;
+ }
+
+ /**
+ * Get the full path to the destination.
+ *
+ * @return string
+ * The full path to the destination file.
+ */
+ public function getDestinationFullPath() {
+ return $this->destinationFullPath;
+ }
+
+ /**
+ * Set the full path to the source.
+ *
+ * @param string $sourceFullPath
+ * The full path to the source file.
+ *
+ * @return $this
+ */
+ public function setSourceFullPath(string $sourceFullPath) {
+ $this->sourceFullPath = $sourceFullPath;
+ return $this;
+ }
+
+ /**
+ * Get the full path to the source.
+ *
+ * @return string
+ * The full path to the source file.
+ */
+ public function getSourceFullPath() {
+ return $this->sourceFullPath;
+ }
+
+ /**
+ * Return the package name that provides the scaffold file info at this destination path.
+ *
+ * Given the list of all scaffold file info objects, return the package that
+ * provides the scaffold file info for the scaffold file that will be placed
+ * at the destination that this scaffold file would be placed at. Note that
+ * this will be the same as $this->getPackageName() unless this scaffold file
+ * has been overridden or removed by some other package.
+ *
+ * @param string[] $list_of_scaffold_files
+ * The list of all scaffold file info objects, keyed by destination path
+ * (and therefore containing only those files being processed)
+ *
+ * @return string
+ * The name of the package that provided the scaffold file information.
+ */
+ public function findProvidingPackage(array $list_of_scaffold_files) {
+ if (!array_key_exists($this->getDestinationRelativePath(), $list_of_scaffold_files)) {
+ throw new \Exception("Scaffold file not found in list of all scaffold files.");
+ }
+ $scaffold_file = $list_of_scaffold_files[$this->getDestinationRelativePath()];
+ return $scaffold_file->getPackageName();
+ }
+
+ /**
+ * Determine whether this scaffold file info is for a destination path that was removed.
+ *
+ * @return bool
+ * True if this scaffold file was removed.
+ */
+ public function removed() {
+ return empty($this->getSourceRelativePath());
+ }
+
+ /**
+ * Determine if this scaffold file has been overridden by another package.
+ *
+ * @param string $providing_package
+ * The name of the package that provides the scaffold file at this location,
+ * as returned by self::findProvidingPackage()
+ *
+ * @return bool
+ * Whether this scaffold file if overridden or removed.
+ */
+ public function overridden(string $providing_package) {
+ return $this->getPackageName() !== $providing_package;
+ }
+
+ /**
+ * Interpolate a string using the data from this scaffold file info.
+ */
+ public function interpolate(string $message, array $extra = [], $default = FALSE) {
+ $interploator = new Interpolator();
+
+ $data = [
+ 'package-name' => $this->getPackageName(),
+ 'dest-rel-path' => $this->getDestinationRelativePath(),
+ 'src-rel-path' => $this->getSourceRelativePath(),
+ 'dest-full-path' => $this->getDestinationFullPath(),
+ 'src-full-path' => $this->getSourceFullPath(),
+ ] + $extra;
+
+ return $interploator->interpolateData($data, $message, $default);
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/composer.json b/core/lib/Drupal/Component/Scaffold/composer.json
index 90cefe4916..6f4c17fce0 100644
--- a/core/lib/Drupal/Component/Scaffold/composer.json
+++ b/core/lib/Drupal/Component/Scaffold/composer.json
@@ -1,25 +1,24 @@
{
- "name": "drupal/drupal-scaffold",
- "description": "New Composer plugin for updating the Drupal scaffold files when using drupal/core",
- "type": "composer-plugin",
- "license": "GPL-2.0-or-later",
- "require": {
- "php": ">=5.5.9",
- "composer-plugin-api": "^1.0.0"
- },
- "require-dev": {
- "composer/composer": "^1.8",
- "symfony/process": "~3.4.0"
- },
- "autoload": {
- "psr-4": {
- "Drupal\\Component\\Scaffold\\": "."
- }
- },
- "extra": {
- "class": "Drupal\\Component\\Scaffold\\Plugin"
- },
- "suggest": {
- "hirak/prestissimo": "Allow simultaneous downloads of multiple scaffold files."
+ "name": "drupal/drupal-scaffold",
+ "description": "Test for Composer Scaffold.",
+ "type": "composer-plugin",
+ "license": "GPL-2.0-or-later",
+ "require": {
+ "php": ">=7.0.8",
+ "composer-plugin-api": "^1.0.0"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "autoload": {
+ "psr-4": {
+ "Drupal\\Component\\Scaffold\\": ""
}
+ },
+ "extra": {
+ "class": "Drupal\\Component\\Scaffold\\Plugin"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0",
+ "phpunit/phpunit": "^4.8.35 || ^6.5"
+ }
}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/FetcherTest.php b/core/tests/Drupal/Tests/Component/Scaffold/FetcherTest.php
deleted file mode 100644
index 83148bdb04..0000000000
--- a/core/tests/Drupal/Tests/Component/Scaffold/FetcherTest.php
+++ /dev/null
@@ -1,74 +0,0 @@
-fs = new Filesystem();
- $this->tmpDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'drupal-scaffold';
- $this->ensureDirectoryExistsAndClear($this->tmpDir);
-
- chdir($this->tmpDir);
- }
-
- /**
- * Makes sure the given directory exists and has no content.
- *
- * @param string $directory
- */
- protected function ensureDirectoryExistsAndClear($directory) {
- if (is_dir($directory)) {
- $this->fs->removeDirectory($directory);
- }
- mkdir($directory, 0777, TRUE);
- }
-
- public function testFetch() {
- $fetcher = new FileFetcher(new RemoteFilesystem(new NullIO()), 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}', new NullIO());
- $fetcher->setFilenames([
- '.htaccess' => '.htaccess',
- 'sites/default/default.settings.php' => 'sites/default/default.settings.php',
- ]);
- $fetcher->fetch('8.1.1', $this->tmpDir, TRUE);
- $this->assertFileExists($this->tmpDir . '/.htaccess');
- $this->assertFileExists($this->tmpDir . '/sites/default/default.settings.php');
- }
-
- public function testInitialFetch() {
- $fetcher = new FileFetcher(new RemoteFilesystem(new NullIO()), 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}', new NullIO());
- $fetcher->setFilenames([
- 'sites/default/default.settings.php' => 'sites/default/settings.php',
- ]);
- $fetcher->fetch('8.1.1', $this->tmpDir, FALSE);
- $this->assertFileExists($this->tmpDir . '/sites/default/settings.php');
- }
-
-}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/HandlerTest.php b/core/tests/Drupal/Tests/Component/Scaffold/HandlerTest.php
new file mode 100644
index 0000000000..d26b9843c7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/HandlerTest.php
@@ -0,0 +1,161 @@
+composer = $this->prophesize(Composer::class);
+ $this->io = $this->prophesize(IOInterface::class);
+ }
+
+ /**
+ * @covers ::getPackageFileMappings
+ */
+ public function testGetPackageFileMappingsErrors() {
+ // Check missing parameters sets appropriate error.
+ $package = $this->prophesize(PackageInterface::class);
+ $package->getExtra()->willReturn([]);
+ $package->getName()->willReturn('foo/bar');
+ $this->io->writeError('The allowed package foo/bar does not provide a file mapping for Composer Scaffold.')->shouldBeCalled();
+ $fixture = new Handler($this->composer->reveal(), $this->io->reveal());
+ $this->assertSame([], $fixture->getPackageFileMappings($package->reveal()));
+
+ // With only one of the required parameters.
+ $package->getExtra()->willReturn(['composer-scaffold' => []]);
+ $this->assertSame([], $fixture->getPackageFileMappings($package->reveal()));
+ }
+
+ /**
+ * @covers ::getPackageFileMappings
+ */
+ public function testGetPackageFileMappings() {
+ $expected = [
+ 'self' => [
+ 'assets/.htaccess' => FALSE,
+ 'assets/robots-default.txt' => '[web-root]/robots.txt',
+ ],
+ ];
+
+ $package = $this->prophesize(PackageInterface::class);
+ $package->getExtra()->willReturn(['composer-scaffold' => ['file-mapping' => $expected]]);
+ $fixture = new Handler($this->composer->reveal(), $this->io->reveal());
+ $this->assertSame($expected, $fixture->getPackageFileMappings($package->reveal()));
+ }
+
+ /**
+ * @covers ::getWebRoot
+ * @covers ::getOptions
+ */
+ public function testGetWebRoot() {
+ $expected = './build/docroot';
+ $extra = [
+ 'composer-scaffold' => [
+ 'locations' => [
+ 'web-root' => $expected,
+ ],
+ ],
+ ];
+
+ $package = $this->prophesize(PackageInterface::class);
+ $package->getExtra()->willReturn($extra);
+
+ $this->composer->getPackage()->willReturn($package->reveal());
+
+ $fixture = new Handler($this->composer->reveal(), $this->io->reveal());
+ $this->assertSame($expected, $fixture->getWebRoot());
+
+ // Verify correct errors.
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('The extra.composer-scaffold.location.web-root is not set in composer.json.');
+ $extra = [
+ 'allowed-packages' => [
+ 'foo/bar',
+ ],
+ ];
+ $package->getExtra()->willReturn($extra);
+ $this->composer->getPackage()->willReturn($package->reveal());
+ $fixture = new Handler($this->composer->reveal(), $this->io->reveal());
+ $fixture->getWebRoot();
+ }
+
+ /**
+ * Tests ArrayManipulator::arrayMergeRecursiveExceptEmpty().
+ *
+ * @dataProvider providerTestArrayMergeRecursiveDistinct
+ *
+ * @covers ::getWebRoot
+ */
+ public function testArrayMergeRecursiveDistinct(
+ array $array1,
+ array $array2,
+ array $expected_array
+ ) {
+ $this->assertSame(Handler::arrayMergeRecursiveDistinct($array1,
+ $array2), $expected_array);
+ }
+
+ /**
+ * Provides values to testArrayMergeRecursiveDistinct().
+ *
+ * @return array
+ * An array of values to test.
+ */
+ public function providerTestArrayMergeRecursiveDistinct() :array {
+ return [
+ [
+ [
+ "drupal/core" => [
+ "assets/.htaccess" => "[web-root]/.htaccess",
+ "assets/robots-default.txt" => "[web-root]/robots.txt",
+ "assets/index.php" => "[web-root]/index.php",
+ ],
+ ],
+ [
+ "drupal/core" => [
+ "assets/.htaccess" => FALSE,
+ "assets/robots-default.txt" => "[web-root]/robots.txt.bak",
+ ],
+ ],
+ [
+ "drupal/core" => [
+ "assets/.htaccess" => FALSE,
+ "assets/robots-default.txt" => "[web-root]/robots.txt.bak",
+ "assets/index.php" => "[web-root]/index.php",
+ ],
+ ],
+ ],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/PluginTest.php b/core/tests/Drupal/Tests/Component/Scaffold/PluginTest.php
deleted file mode 100644
index 17be8c51bb..0000000000
--- a/core/tests/Drupal/Tests/Component/Scaffold/PluginTest.php
+++ /dev/null
@@ -1,184 +0,0 @@
-fs = new Filesystem();
- // @todo: Change this to be the component root dir when we start testing
- // components in isolation.
- // https://www.drupal.org/project/drupal/issues/2943856
- $this->drupalRootDir = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
- // Get the path to the Scaffold component.
- $this->componentRootDir = $this->drupalRootDir . '/core/lib/Drupal/Component/Scaffold';
-
- // Prepare temp directory. If the simpletest directory is present, then we
- // want our temp dir to be inside it.
- $this->tmpDir = $this->drupalRootDir . '/sites/simpletest';
- if (is_dir($this->tmpDir)) {
- $this->tmpDir .= '/drupal-scaffold';
- }
- else {
- $this->tmpDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'drupal-scaffold';
- }
- $this->ensureDirectoryExistsAndClear($this->tmpDir);
- $this->writeComposerJSON();
-
- chdir($this->tmpDir);
- }
-
- /**
- * Clean up our temporary build directory.
- */
- public function tearDown() {
- $this->fs->removeDirectory($this->tmpDir);
- }
-
- /**
- * Tests a simple composer install without core, but adding core later.
- */
- public function testComposerInstallAndUpdate() {
- $exampleScaffoldFile = $this->tmpDir . DIRECTORY_SEPARATOR . 'index.php';
-
- $this->assertFileNotExists($exampleScaffoldFile, 'Scaffold file should not be exist.');
- $this->composer('install --no-dev --prefer-dist');
- $this->assertFileExists($this->tmpDir . DIRECTORY_SEPARATOR . 'core', 'Drupal core is installed.');
- $this->assertFileExists($exampleScaffoldFile, 'Scaffold file should be automatically installed.');
-
- $this->fs->remove($exampleScaffoldFile);
- $this->assertFileNotExists($exampleScaffoldFile, 'Scaffold file should not exist.');
- $this->composer('drupal:scaffold');
- $this->assertFileExists($exampleScaffoldFile, 'Scaffold file should be installed by "drupal:scaffold" command.');
-
- // We touch a scaffold file, so we can check the file was modified after the
- // scaffold update.
- $version = '8.7.x-dev';
- touch($exampleScaffoldFile);
- $mtime_touched = filemtime($exampleScaffoldFile);
- // Requiring a newer version triggers "composer update".
- $this->composer('require --update-with-dependencies --prefer-dist --update-no-dev drupal/core:"' . $version . '"');
- clearstatcache();
- $mtime_after = filemtime($exampleScaffoldFile);
- $this->assertNotEquals($mtime_after, $mtime_touched, 'Scaffold file was modified by composer update. (' . $version . ')');
-
- // We touch a scaffold file, so we can check the file was modified by the
- // custom command.
- file_put_contents($exampleScaffoldFile, 1);
- $this->composer('drupal:scaffold');
- $this->assertNotEquals(file_get_contents($exampleScaffoldFile), 1, 'Scaffold file was modified by custom command.');
- }
-
- /**
- * Writes the default composer json to the temp direcoty.
- */
- protected function writeComposerJSON() {
- $json = json_encode($this->composerJSONDefaults(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- // Write composer.json.
- file_put_contents($this->tmpDir . '/composer.json', $json);
- }
-
- /**
- * Provides the default composer.json data.
- *
- * @return array
- */
- protected function composerJSONDefaults() {
- $package = json_decode(file_get_contents($this->componentRootDir . '/composer.json'), TRUE);
- $package['dist'] = [
- 'type' => 'path',
- 'url' => $this->componentRootDir,
- ];
- $package['version'] = '999.0.' . time();
- return [
- 'repositories' => [
- [
- 'type' => 'package',
- 'package' => $package,
- ],
- ],
- 'require' => [
- 'composer/installers' => '^1.0.20',
- 'drupal/drupal-scaffold' => $package['version'],
- 'drupal/core' => '8.6.0',
- ],
- 'minimum-stability' => 'dev',
- 'prefer-stable' => TRUE,
- ];
- }
-
- /**
- * Wrapper for the composer command.
- *
- * @param string $command
- * Composer command name, arguments and/or options.
- */
- protected function composer($command) {
- $commands = [
- $this->drupalRootDir . '/vendor/bin/composer',
- $command,
- ];
- $ps = new Process(implode(' ', $commands), $this->tmpDir);
- // While some commands may take more than 60 seconds without output, the
- // whole process should not take longer than 5 minutes. Adjust this as
- // needed.
- $ps->setTimeout(300)
- ->setIdleTimeout(NULL)
- ->mustRun();
- }
-
- /**
- * Makes sure the given directory exists and has no content.
- *
- * @param string $directory
- */
- protected function ensureDirectoryExistsAndClear($directory) {
- if (is_dir($directory)) {
- $this->fs->removeDirectory($directory);
- }
- mkdir($directory, 0777, TRUE);
- }
-
-}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/ScaffoldTest.php b/core/tests/Drupal/Tests/Component/Scaffold/ScaffoldTest.php
new file mode 100644
index 0000000000..b5240d27ee
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/ScaffoldTest.php
@@ -0,0 +1,230 @@
+fileSystem = new Filesystem();
+
+ $this->projectRoot = realpath(__DIR__ . '/../../../../../lib/Drupal/Component/Scaffold');
+ $this->fixtures = sys_get_temp_dir() . '/composer-scaffold-test-' . md5($this->getName() . microtime());
+ }
+
+ /**
+ * Create the System-Under-Test.
+ */
+ protected function createSut($topLevelProjectDir, $replacements = []) {
+ $replacements += [
+ 'SYMLINK' => 'true',
+ ];
+ $interpolator = new Interpolator('__', '__', TRUE);
+ $interpolator->setData($replacements);
+ $projectRoot = dirname(__DIR__);
+ $this->sut = $this->fixtures . '/' . $topLevelProjectDir;
+
+ $this->fileSystem->copy(realpath(__DIR__ . '/fixtures'), $this->fixtures);
+
+ $composer_json_templates = glob($this->fixtures . "/*/composer.json.tmpl");
+ foreach ($composer_json_templates as $composer_json_tmpl) {
+ // Inject replacements into composer.json.
+ if (file_exists($composer_json_tmpl)) {
+ $composer_json_contents = file_get_contents($composer_json_tmpl);
+ $composer_json_contents = $interpolator->interpolate($composer_json_contents, FALSE);
+ file_put_contents(dirname($composer_json_tmpl) . "/composer.json", $composer_json_contents);
+ @unlink($composer_json_tmpl);
+ }
+ }
+
+ return $this->sut;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown() {
+ // Remove the fixture filesystem.
+ $this->fileSystem->remove($this->fixtures);
+ }
+
+ /**
+ * Data provider for testScaffoldFixturesThatThrow().
+ */
+ public function scaffoldFixturesThatMakeErrors() {
+ return [
+ 'missing-robots-txt' => [
+ 'drupal-drupal-missing-scaffold-file',
+ '_no_assertion_',
+ TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * Tests that scaffold files return nonzero when they have bad values.
+ *
+ * @dataProvider scaffoldFixturesThatMakeErrors
+ */
+ public function testScaffoldFixturesError($topLevelProjectDir, $scaffoldAssertions, $is_link) {
+ $sut = $this->createSut($topLevelProjectDir, [
+ 'SYMLINK' => $is_link ? 'true' : 'false',
+ 'PROJECT_ROOT' => $this->projectRoot,
+ ]);
+
+ // Test composer install. Expect an error.
+ // @todo: assert output contains too.
+ $this->runComposer("install", 1, 'Could not find source file');
+ }
+
+ /**
+ * Data provider for testComposerInstallScaffold and testScaffoldCommand.
+ */
+ public function scaffoldTestValues() {
+ return [
+ [
+ 'drupal-composer-drupal-project',
+ 'assertDrupalProjectSutWasScaffolded',
+ TRUE,
+ ],
+ [
+ 'drupal-drupal',
+ 'assertDrupalDrupalSutWasScaffolded',
+ FALSE,
+ ],
+ ];
+ }
+
+ /**
+ * Tests that scaffold files are correctly moved by the plugin.
+ *
+ * @dataProvider scaffoldTestValues
+ */
+ public function testScaffoldPlugin($topLevelProjectDir, $scaffoldAssertions, $is_link) {
+ $sut = $this->createSut($topLevelProjectDir, [
+ 'SYMLINK' => $is_link ? 'true' : 'false',
+ 'PROJECT_ROOT' => $this->projectRoot,
+ ]);
+
+ // Test composer install.
+ $this->runComposer('install');
+ call_user_func([$this, $scaffoldAssertions], $sut, $is_link, $topLevelProjectDir);
+
+ // Test composer:scaffold.
+ $this->runComposer('composer:scaffold');
+ call_user_func([$this, $scaffoldAssertions], $sut, $is_link, $topLevelProjectDir);
+ }
+
+ /**
+ * Runs a `composer` command.
+ */
+ protected function runComposer($cmd, $expectedExitCode = 0, $expectedContents = '') {
+ $process = new Process("composer $cmd", $this->sut);
+ $process->setTimeout(300)->setIdleTimeout(300)->run();
+ if (!empty($expectedContents)) {
+ $this->assertContains($expectedContents, $process->getOutput() . "\n" . $process->getErrorOutput());
+ }
+ $this->assertSame($expectedExitCode, $process->getExitCode(), $process->getErrorOutput());
+ }
+
+ /**
+ * Asserts that scaffold files were correctly moved.
+ */
+ protected function assertDrupalProjectSutWasScaffolded($sut, $is_link, $project_name) {
+ $this->assertDrupalRootWasScaffolded($sut . '/docroot', $is_link, $project_name);
+ }
+
+ /**
+ * Asserts that scaffold files were correctly moved.
+ */
+ protected function assertDrupalDrupalSutWasScaffolded($sut, $is_link, $project_name) {
+ $this->assertDrupalRootWasScaffolded($sut, $is_link, $project_name);
+ }
+
+ /**
+ * Assert that the scaffold files are placed as we expect them to be.
+ */
+ protected function assertDrupalRootWasScaffolded($docroot, $is_link, $project_name) {
+ $from_project = "scaffolded from \"file-mappings\" in $project_name composer.json fixture";
+ $from_scaffold_override = 'scaffolded from the scaffold-override-fixture';
+ $from_core = 'from drupal/core';
+
+ // Ensure that the autoload.php file was written.
+ $this->assertFileExists($docroot . '/autoload.php');
+
+ // Assert other scaffold files are written in the correct locations.
+ $this->assertScaffoldedFile($docroot . '/.csslintrc', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.editorconfig', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.eslintignore', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.eslintrc.json', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.gitattributes', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.ht.router.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/sites/default/default.services.yml', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/sites/default/default.settings.php', $is_link, $from_scaffold_override);
+ $this->assertScaffoldedFile($docroot . '/sites/example.settings.local.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/sites/example.sites.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/index.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/robots.txt', $is_link, $from_project);
+ $this->assertScaffoldedFile($docroot . '/update.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/web.config', $is_link, $from_core);
+
+ // Ensure that the .htaccess.txt file was not written, as our
+ // top-level composer.json excludes it from the files to scaffold.
+ $this->assertFileNotExists($docroot . '/.htaccess');
+ }
+
+ /**
+ * Asserts that a given file exists and is/is not a symlink.
+ */
+ protected function assertScaffoldedFile($path, $is_link, $contents_contains) {
+ $this->assertFileExists($path);
+ $contents = file_get_contents($path);
+ $this->assertContains($contents_contains, basename($path) . ': ' . $contents);
+ $this->assertSame($is_link, is_link($path));
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md
new file mode 100644
index 0000000000..dd4d9c73e8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md
@@ -0,0 +1,5 @@
+# Fixtures README
+
+To use these fixtures, copy them to a temporary directory located next to the project directory.
+
+Alternately, copy them to any other location, and then fix up the path to composer-scaffold in the composer.json file in the fixture being tested (e.g. drupal-composer-drupal-project).
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/assets/robots-default.txt
new file mode 100644
index 0000000000..c29217ef39
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in drupal-composer-drupal-project composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl
new file mode 100644
index 0000000000..0cf5baf548
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl
@@ -0,0 +1,65 @@
+{
+ "name": "my/project",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal": {
+ "type": "composer",
+ "url": "https://packages.drupal.org/8"
+ }
+ },
+ "require": {
+ "drupal/drupal-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./docroot"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "docroot/core": ["type:drupal-core"],
+ "docroot/modules/contrib/{$name}": ["type:drupal-module"],
+ "docroot/modules/custom/{$name}": ["type:drupal-custom-module"],
+ "docroot/profiles/contrib/{$name}": ["type:drupal-profile"],
+ "docroot/profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "docroot/themes/contrib/{$name}": ["type:drupal-theme"],
+ "docroot/themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "docroot/libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.csslintrc b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.csslintrc
new file mode 100644
index 0000000000..f5bca65208
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.csslintrc
@@ -0,0 +1 @@
+# Test version of .csslintrc from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.editorconfig b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.editorconfig
new file mode 100644
index 0000000000..dcf0c98bbf
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.editorconfig
@@ -0,0 +1 @@
+# Test version of .editorconfig from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.eslintignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.eslintignore
new file mode 100644
index 0000000000..f0405c03ad
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.eslintignore
@@ -0,0 +1 @@
+# Test version of .eslintignore from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.eslintrc.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.eslintrc.json
new file mode 100644
index 0000000000..6391fb0c6e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.eslintrc.json
@@ -0,0 +1 @@
+// Test version of .eslintrc.json from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.gitattributes b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.gitattributes
new file mode 100644
index 0000000000..03436baebc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.gitattributes
@@ -0,0 +1 @@
+# Test version of .gitattributes from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.ht.router.php b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.ht.router.php
new file mode 100644
index 0000000000..0cd34ad7ad
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/assets/.ht.router.php
@@ -0,0 +1,2 @@
+
+
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json
new file mode 100644
index 0000000000..bff69c4436
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "fixtures/drupal-core-fixture",
+ "extra": {
+ "composer-scaffold": {
+ "file-mapping": {
+ "[web-root]/.csslintrc": "assets/.csslintrc",
+ "[web-root]/.editorconfig": "assets/.editorconfig",
+ "[web-root]/.eslintignore": "assets/.eslintignore",
+ "[web-root]/.eslintrc.json": "assets/.eslintrc.json",
+ "[web-root]/.gitattributes": "assets/.gitattributes",
+ "[web-root]/.ht.router.php": "assets/.ht.router.php",
+ "[web-root]/.htaccess": "assets/.htaccess",
+ "[web-root]/sites/default/default.services.yml": "assets/default.services.yml",
+ "[web-root]/sites/default/default.settings.php": "assets/default.settings.php",
+ "[web-root]/sites/example.settings.local.php": "assets/example.settings.local.php",
+ "[web-root]/sites/example.sites.php": "assets/example.sites.php",
+ "[web-root]/index.php": "assets/index.php",
+ "[web-root]/robots.txt": "assets/robots.txt",
+ "[web-root]/update.php": "assets/update.php",
+ "[web-root]/web.config": "assets/web.config"
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl
new file mode 100644
index 0000000000..e91c781e03
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl
@@ -0,0 +1,65 @@
+{
+ "name": "my/project",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "drupal-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal": {
+ "type": "composer",
+ "url": "https://packages.drupal.org/8"
+ }
+ },
+ "require": {
+ "drupal/drupal-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/missing-robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/assets/robots-default.txt
new file mode 100644
index 0000000000..6eb30e86aa
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl
new file mode 100644
index 0000000000..dc026c6bfd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl
@@ -0,0 +1,65 @@
+{
+ "name": "my/project",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal": {
+ "type": "composer",
+ "url": "https://packages.drupal.org/8"
+ }
+ },
+ "require": {
+ "drupal/drupal-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php
new file mode 100644
index 0000000000..f37a684279
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php
@@ -0,0 +1,6 @@
+