diff --git a/core/composer.json b/core/composer.json index 4955b07648..118421730f 100644 --- a/core/composer.json +++ b/core/composer.json @@ -110,6 +110,7 @@ "drupal/core-proxy-builder": "self.version", "drupal/core-render": "self.version", "drupal/core-serialization": "self.version", + "drupal/core-composer-scaffold": "self.version", "drupal/core-transliteration": "self.version", "drupal/core-utility": "self.version", "drupal/core-uuid": "self.version", @@ -200,6 +201,7 @@ "core/lib/Drupal/Component/ProxyBuilder/composer.json", "core/lib/Drupal/Component/Render/composer.json", "core/lib/Drupal/Component/Serialization/composer.json", + "core/lib/Drupal/Component/Scaffold/composer.json", "core/lib/Drupal/Component/Transliteration/composer.json", "core/lib/Drupal/Component/Utility/composer.json", "core/lib/Drupal/Component/Uuid/composer.json", diff --git a/core/lib/Drupal/Component/Scaffold/AllowedPackages.php b/core/lib/Drupal/Component/Scaffold/AllowedPackages.php new file mode 100644 index 0000000000..edfc39bcc3 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/AllowedPackages.php @@ -0,0 +1,163 @@ +composer = $composer; + $this->io = $io; + $this->manageOptions = $manage_options; + } + + /** + * Gets a list of all packages that are allowed to copy scaffold files. + * + * 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. + */ + public function getAllowedPackages() { + $options = $this->manageOptions->getOptions(); + $allowed_packages = $this->recursiveGetAllowedPackages($options->allowedPackages()); + // If the root package defines any file mappings, then implicitly add it + // to the list of allowed packages. Add it at the end so that it overrides + // all the preceding packages. + if ($options->hasFileMapping()) { + $root_package = $this->composer->getPackage(); + unset($allowed_packages[$root_package->getName()]); + $allowed_packages[$root_package->getName()] = $root_package; + } + // Handle any newly-added packages that are not already allowed. + $allowed_packages = $this->evaluateNewPackages($allowed_packages); + return $allowed_packages; + } + + /** + * {@inheritdoc} + */ + public function event(PackageEvent $event) { + $operation = $event->getOperation(); + // Determine the package. + $package = $operation->getJobType() == 'update' ? $operation->getTargetPackage() : $operation->getPackage(); + if (ScaffoldOptions::hasOptions($package->getExtra())) { + $this->newPackages[$package->getName()] = $package; + } + } + + /** + * Builds a name-to-package mapping from a list of package names. + * + * @param string[] $packages_to_allow + * List of package names to allow. + * @param array $allowed_packages + * Mapping of package names to PackageInterface of packages already + * accumulated. + * + * @return \Composer\Package\PackageInterface[] + * Mapping of package names to PackageInterface in priority order. + */ + protected function recursiveGetAllowedPackages(array $packages_to_allow, array $allowed_packages = []) { + foreach ($packages_to_allow as $name) { + $package = $this->getPackage($name); + if ($package && $package instanceof PackageInterface && !array_key_exists($name, $allowed_packages)) { + $allowed_packages[$name] = $package; + $packageOptions = $this->manageOptions->packageOptions($package); + $allowed_packages = $this->recursiveGetAllowedPackages($packageOptions->allowedPackages(), $allowed_packages); + } + } + return $allowed_packages; + } + + /** + * Evaluates newly-added packages and see if they are already allowed. + * + * For now we will only emit warnings if they are not. + * + * @param array $allowed_packages + * Mapping of package names to PackageInterface of packages already + * accumulated. + * + * @return \Composer\Package\PackageInterface[] + * Mapping of package names to PackageInterface in priority order. + */ + protected function evaluateNewPackages(array $allowed_packages) { + foreach ($this->newPackages as $name => $newPackage) { + if (!array_key_exists($name, $allowed_packages)) { + $this->io->write("Package {$name} has scaffold operations, but it is not allowed in the root-level composer.json file."); + } + else { + $this->io->write("Package {$name} has scaffold operations, and is already allowed in the root-level composer.json file."); + } + } + // @todo We could prompt the user and ask if they wish to allow a newly-added package. + return $allowed_packages; + } + + /** + * Retrieves a package from the current composer process. + * + * @param string $name + * Name of the package to get from the current composer installation. + * + * @return \Composer\Package\PackageInterface|null + * The Composer package. + */ + protected function getPackage($name) { + return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*'); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/CommandProvider.php b/core/lib/Drupal/Component/Scaffold/CommandProvider.php new file mode 100644 index 0000000000..9ec63b4a0d --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/CommandProvider.php @@ -0,0 +1,19 @@ +setName('composer:scaffold') + ->setDescription('Update the Composer scaffold files.') + ->setHelp( + <<composer:scaffold command places the scaffold files in their +respective locations according to the layout stipulated in the composer.json +file. + +php composer.phar composer:scaffold + +It is usually not necessary to call composer:scaffold manually, +because it is called automatically as needed, e.g. after an install +or update command. Note, though, that only packages explicitly +allowed to scaffold in the top-level composer.json will be processed by this +command. + +For more information, see https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold. +EOT + ); + + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $handler = new Handler($this->getComposer(), $this->getIO()); + $handler->scaffold(); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php b/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php new file mode 100644 index 0000000000..64b240ec42 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php @@ -0,0 +1,97 @@ +fullPath()); + // 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, realpath($location)); + $fs->dumpFile($autoload_path->fullPath(), static::autoLoadContents($relative_vendor_path)); + return new ScaffoldResult($autoload_path, TRUE); + } + + /** + * Generates a scaffold file path object for the autoload file. + * + * @param string $package_name + * The name of the package defining the autoload file (the root package). + * @param string $web_root + * The path to the web root. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * Object wrapping the relative and absolute path to the destination file. + */ + protected static function autoloadPath($package_name, $web_root) { + $rel_path = 'autoload.php'; + $dest_rel_path = '[web-root]/' . $rel_path; + $dest_full_path = $web_root . '/' . $rel_path; + return new ScaffoldFilePath('autoload', $package_name, $dest_rel_path, $dest_full_path); + } + + /** + * Builds the contents of the autoload file. + * + * @param string $vendor_path + * The relative path to vendor. + * + * @return string + * Return the contents for the autoload.php. + */ + protected static function autoLoadContents($vendor_path) { + $vendor_path = rtrim($vendor_path, '/'); + return <<composer = $composer; + $this->io = $io; + $this->manageOptions = new ManageOptions($composer); + $this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions); + } + + /** + * Registers post-package events before any 'require' event runs. + * + * This method is called by composer prior to doing a 'require' command. + * + * @param \Composer\Plugin\CommandEvent $event + * The Composer Command event. + */ + public function beforeRequire(CommandEvent $event) { + // In order to differentiate between post-package events called after + // 'composer require' vs. the same events called at other times, we will + // only install our handler when a 'require' event is detected. + $this->postPackageListeners[] = $this->manageAllowedPackages; + } + + /** + * Posts package command event. + * + * We want to detect packages 'require'd that have scaffold files, but are not + * yet allowed in the top-level composer.json file. + * + * @param \Composer\Installer\PackageEvent $event + * Composer package event sent on install/update/remove. + */ + public function onPostPackageEvent(PackageEvent $event) { + foreach ($this->postPackageListeners as $listener) { + $listener->event($event); + } + } + + /** + * Creates scaffold operation objects for all items in the file mappings. + * + * @param \Composer\Package\PackageInterface $package + * The package that relative paths will be relative from. + * @param array $package_file_mappings + * The package file mappings array keyed by destination path and the values + * are operation metadata arrays. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface[] + * A list of scaffolding operation objects + */ + protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) { + $scaffoldOpFactory = new OperationFactory($this->composer); + $scaffoldOps = []; + foreach ($package_file_mappings as $dest_rel_path => $metadata) { + $scaffoldOps[$dest_rel_path] = $scaffoldOpFactory->create($package, $dest_rel_path, $metadata); + } + return $scaffoldOps; + } + + /** + * Copies all scaffold files from source to destination. + */ + public function scaffold() { + // Recursively get the list of allowed packages. Only allowed packages + // may declare scaffold files. Note that the top-level composer.json file + // is implicitly allowed. + $allowedPackages = $this->manageAllowedPackages->getAllowedPackages(); + if (empty($allowedPackages)) { + $this->io->write("Nothing scaffolded because no packages are allowed in the top-level composer.json file."); + return; + } + // Call any pre-scaffold scripts that may be defined. + $dispatcher = new EventDispatcher($this->composer, $this->io); + $dispatcher->dispatch(self::PRE_COMPOSER_SCAFFOLD_CMD); + + // Fetch the list of file mappings from each allowed package and normalize + // them. + $file_mappings = $this->getFileMappingsFromPackages($allowedPackages); + + // Analyze the list of file mappings, and determine which take priority. + $scaffoldCollection = new OperationCollection($this->io); + $locationReplacements = $this->manageOptions->getLocationReplacements(); + + // Write the collected scaffold files to the designated location on disk. + $scaffoldResults = $scaffoldCollection->process($file_mappings, $locationReplacements, $this->manageOptions->getOptions()); + + // 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. + $webRoot = $this->manageOptions->getOptions()->getLocation('web-root', FALSE); + if (!$webRoot) { + throw new \RuntimeException("The extra.composer-scaffold.location.webroot is not set in the project's composer.json."); + } + $scaffoldResults[] = GenerateAutoloadReferenceFile::generateAutoload($this->rootPackageName(), $webRoot, $this->getVendorPath()); + + // Add the managed scaffold files to .gitignore if applicable. + $gitIgnoreManager = new ManageGitIgnore(getcwd()); + $gitIgnoreManager->manageIgnored($scaffoldResults, $this->manageOptions->getOptions()); + + // Call post-scaffold scripts. + $dispatcher->dispatch(self::POST_COMPOSER_SCAFFOLD_CMD); + } + + /** + * Gets the path to the 'vendor' directory. + * + * @return string + * The file path of the vendor directory. + */ + protected function getVendorPath() { + $vendorDir = $this->composer->getConfig()->get('vendor-dir'); + $filesystem = new Filesystem(); + return $filesystem->normalizePath(realpath($vendorDir)); + } + + /** + * 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 \Drupal\Component\Scaffold\Operations\OperationInterface[] + * An array of destination paths => scaffold operation objects. + */ + protected function getFileMappingsFromPackages(array $allowed_packages) { + $file_mappings = []; + foreach ($allowed_packages as $package_name => $package) { + $package_file_mappings = $this->getPackageFileMappings($package); + $file_mappings[$package_name] = $package_file_mappings; + } + return $file_mappings; + } + + /** + * Gets the array of file mappings provided by a given package. + * + * @param \Composer\Package\PackageInterface $package + * The Composer package from which to get the file mappings. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface[] + * An array of destination paths => scaffold operation objects. + */ + protected function getPackageFileMappings(PackageInterface $package) { + $options = $this->manageOptions->packageOptions($package); + if ($options->hasFileMapping()) { + return $this->createScaffoldOperations($package, $options->fileMapping()); + } + else { + if (!$options->hasAllowedPackages()) { + $this->io->writeError("The allowed package {$package->getName()} does not provide a file mapping for Composer Scaffold."); + } + return []; + } + } + + /** + * Gets the root package name. + * + * @return string + * The package name of the root project + */ + protected function rootPackageName() { + $root_package = $this->composer->getPackage(); + return $root_package->getName(); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Interpolator.php b/core/lib/Drupal/Component/Scaffold/Interpolator.php new file mode 100644 index 0000000000..a80ea26396 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Interpolator.php @@ -0,0 +1,155 @@ +startToken = $start_token; + $this->endToken = $end_token; + } + + /** + * Sets the data set to use when interpolating. + * + * @param array $data + * Interpolation data to use when interpolating. + * + * @return $this + */ + public function setData(array $data) { + $this->data = $data; + return $this; + } + + /** + * Adds to the data set to use when interpolating. + * + * @param array $data + * Interpolation data to use when interpolating. + * + * @return $this + */ + public function addData(array $data) { + $this->data = array_merge($this->data, $data); + return $this; + } + + /** + * Replaces tokens in a string with values from an associative array. + * + * Tokens are surrounded by delimiters, e.g. square brackets "[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 string $message + * Message containing tokens to be replaced. + * @param array $extra + * Data to use for interpolation in addition to whatever was provided to + * self::setData(). + * @param string|bool $default + * (optional) The value to substitute for tokens that are not found in the + * data. If FALSE, then missing tokens are not replaced. Defaults to an + * empty string. + * + * @return string + * The message after replacements have been made. + */ + public function interpolate($message, array $extra = [], $default = '') { + $data = $extra + $this->data; + $replacements = $this->replacements($message, $data, $default); + return strtr($message, $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. + * + * @param string $message + * String with tokens. + * @param array $data + * Data to use for interpolation. + * @param string $default + * (optional) The value to substitute for tokens that are not found in the + * data. If FALSE, then missing tokens are not replaced. Defaults to an + * empty string. + * + * @return string[] + * An array of replacements to make. Keyed by tokens and the replacements + * are the values. + */ + protected function replacements($message, array $data, $default = '') { + $tokens = $this->findTokens($message); + $replacements = []; + foreach ($tokens as $sourceText => $key) { + $replacementText = array_key_exists($key, $data) ? $data[$key] : $default; + if ($replacementText !== FALSE) { + $replacements[$sourceText] = $replacementText; + } + } + return $replacements; + } + + /** + * 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 + */ + protected 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; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/LICENSE.txt b/core/lib/Drupal/Component/Scaffold/LICENSE.txt new file mode 100644 index 0000000000..94fb84639c --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php b/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php new file mode 100644 index 0000000000..9985f3ca9f --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php @@ -0,0 +1,171 @@ +dir = $dir; + } + + /** + * Manages gitignore files. + * + * @param \Drupal\Component\Scaffold\Operations\ScaffoldResult[] $files + * A list of scaffold results, each of which holds a path and whether + * or not that file is managed. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the composer.json extras section. + */ + public function manageIgnored(array $files, ScaffoldOptions $options) { + if (!$this->managementOfGitIgnoreEnabled($options)) { + return; + } + + // Accumulate entries to add to .gitignore, sorted into buckets based on the + // location of the .gitignore file the entry should be added to. + $addToGitIgnore = []; + foreach ($files as $scaffoldResult) { + $isIgnored = $this->checkIgnore($scaffoldResult->destination()->fullPath()); + if (!$isIgnored) { + $isTracked = $this->checkTracked($scaffoldResult->destination()->fullPath()); + if (!$isTracked && $scaffoldResult->isManaged()) { + $path = $scaffoldResult->destination()->fullPath(); + $dir = realpath(dirname($path)); + $name = basename($path); + $addToGitIgnore[$dir][] = $name; + } + } + } + // Write out the .gitignore files one at a time. + foreach ($addToGitIgnore as $dir => $entries) { + $this->addToGitIgnore($dir, $entries); + } + } + + /** + * Determines whether the specified scaffold file is already ignored. + * + * @param string $path + * Path to scaffold file to check. + * + * @return bool + * Whether the specified file is already ignored or not (TRUE if ignored). + */ + protected function checkIgnore($path) { + $process = new Process('git check-ignore ' . $path, $this->dir); + $process->run(); + $isIgnored = $process->getExitCode() == 0; + return $isIgnored; + } + + /** + * Determines whether the specified scaffold file is tracked by git. + * + * @param string $path + * Path to scaffold file to check. + * + * @return bool + * Whether the specified file is already tracked or not (TRUE if tracked). + */ + protected function checkTracked($path) { + $process = new Process('git ls-files --error-unmatch ' . $path, $this->dir); + $process->run(); + $isTracked = $process->getExitCode() == 0; + return $isTracked; + } + + /** + * Checks to see if the project root dir is in a git repository. + * + * @return bool + * True if this is a repository. + */ + protected function isRepository() { + $process = new Process('git rev-parse --show-toplevel', $this->dir); + $process->run(); + $isRepository = $process->getExitCode() == 0; + return $isRepository; + } + + /** + * Checks to see if the vendor directory is git ignored. + * + * @return bool + * True if 'vendor' is committed, or false if it is ignored. + */ + protected function vendorCommitted() { + return $this->checkTracked('vendor'); + } + + /** + * Determines whether we should manage gitignore files. + * + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the composer.json extras section. + * + * @return bool + * Whether or not gitignore files should be managed. + */ + protected function managementOfGitIgnoreEnabled(ScaffoldOptions $options) { + // If the composer.json stipulates whether gitignore is managed or not, then + // follow its recommendation. + if ($options->hasGitIgnore()) { + return $options->gitIgnore(); + } + + // Do not manage .gitignore if there is no repository here. + if (!$this->isRepository()) { + return FALSE; + } + + // If the composer.json did not specify whether or not .gitignore files + // should be managed, then manage them if the vendor directory is not + // committed. + return !$this->vendorCommitted(); + } + + /** + * Adds a set of entries to the specified .gitignore file. + * + * @param string $dir + * Path to directory where gitignore should be written. + * @param string[] $entries + * Entries to write to .gitignore file. + */ + protected function addToGitIgnore($dir, array $entries) { + sort($entries); + $gitIgnorePath = $dir . '/.gitignore'; + $contents = ''; + + // Appending to existing .gitignore files. + if (file_exists($gitIgnorePath)) { + $contents = file_get_contents($gitIgnorePath); + if (!empty($contents) && substr($contents, -1) != "\n") { + $contents .= "\n"; + } + } + + $contents .= implode("\n", $entries); + file_put_contents($gitIgnorePath, $contents); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/ManageOptions.php b/core/lib/Drupal/Component/Scaffold/ManageOptions.php new file mode 100644 index 0000000000..be69cfb1a2 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ManageOptions.php @@ -0,0 +1,90 @@ +composer = $composer; + } + + /** + * Gets the root-level scaffold options for this project. + * + * @return \Drupal\Component\Scaffold\ScaffoldOptions + * The scaffold options object. + */ + public function getOptions() { + return $this->packageOptions($this->composer->getPackage()); + } + + /** + * Gets the scaffold options for the stipulated project. + * + * @param \Composer\Package\PackageInterface $package + * The package to fetch the scaffold options from. + * + * @return \Drupal\Component\Scaffold\ScaffoldOptions + * The scaffold options object. + */ + public function packageOptions(PackageInterface $package) { + return ScaffoldOptions::create($package->getExtra()); + } + + /** + * Creates an interpolator for the 'locations' element. + * + * The interpolator returned will replace a path string with the tokens + * defined in the 'locations' element. + * + * Note that only the root package may define locations. + * + * @return \Drupal\Component\Scaffold\Interpolator + * Interpolator that will do replacements in a string using tokens in + * 'locations' element. + */ + public function getLocationReplacements() { + return (new Interpolator())->setData($this->ensureLocations()); + } + + /** + * Ensures that all of the locations defined in the scaffold filed exist. + * + * Create them on the filesystem if they do not. + */ + protected function ensureLocations() { + $fs = new Filesystem(); + $locations = $this->getOptions()->locations() + ['web_root' => './']; + $locations = array_map(function ($location) use ($fs) { + $fs->ensureDirectoryExists($location); + $location = realpath($location); + return $location; + }, $locations); + return $locations; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php b/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php new file mode 100644 index 0000000000..d8ca757a4d --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php @@ -0,0 +1,79 @@ +prepend = $prepend_path; + $this->append = $append_path; + } + + /** + * {@inheritdoc} + */ + public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { + $destination_path = $destination->fullPath(); + if (!file_exists($destination_path)) { + throw new \RuntimeException($destination->getInterpolator()->interpolate("Cannot append/prepend because no prior package provided a scaffold file at that [dest-rel-path].")); + } + $interpolator = $destination->getInterpolator(); + + // Fetch the prepend contents, if provided. + $prependContents = ''; + if (!empty($this->prepend)) { + $this->prepend->addInterpolationData($interpolator, 'prepend'); + $prependContents = file_get_contents($this->prepend->fullPath()) . "\n"; + $io->write($interpolator->interpolate(" - Prepend to [dest-rel-path] from [prepend-rel-path]")); + } + // Fetch the append contents, if provided. + $appendContents = ''; + if (!empty($this->append)) { + $this->append->addInterpolationData($interpolator, 'append'); + $appendContents = "\n" . file_get_contents($this->append->fullPath()); + $io->write($interpolator->interpolate(" - Append to [dest-rel-path] from [append-rel-path]")); + } + if (!empty(trim($prependContents)) || !empty(trim($appendContents))) { + // Assume that none of these files is very large, so load them all into + // memory for now. Considering uses streams to scaffold large files. + $originalContents = file_get_contents($destination_path); + // Write the appended and prepended contents back to the file. + $alteredContents = $prependContents . $originalContents . $appendContents; + file_put_contents($destination_path, $alteredContents); + } + else { + $io->write($interpolator->interpolate(" - Keep [dest-rel-path] unchanged: no content to prepend / append was provided.")); + } + return new ScaffoldResult($destination, TRUE); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/ConjoinableInterface.php b/core/lib/Drupal/Component/Scaffold/Operations/ConjoinableInterface.php new file mode 100644 index 0000000000..4f494173fa --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/ConjoinableInterface.php @@ -0,0 +1,15 @@ +firstOperation = $first_operation; + $this->secondOperation = $second_operation; + } + + /** + * {@inheritdoc} + */ + public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { + $destination_path = $destination->fullPath(); + // First, scaffold the original file. Disable symlinking, because we + // need a copy of the file if we're going to append / prepend to it. + @unlink($destination_path); + $this->firstOperation->process($destination, $io, $options->overrideSymlink(FALSE)); + return $this->secondOperation->process($destination, $io, $options); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php b/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php new file mode 100644 index 0000000000..ce2c00ddfd --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php @@ -0,0 +1,156 @@ +io = $io; + } + + /** + * Finds the package name that provides the scaffold file. + * + * 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 $scaffold_file->packageName() unless this scaffold + * file has been overridden or removed by some other package. + * + * @param \Drupal\Component\Scaffold\ScaffoldFileInfo[] $list_of_scaffold_files + * Associative array containing destination => operation mappings. + * @param \Drupal\Component\Scaffold\ScaffoldFileInfo $scaffold_file + * The scaffold file to use to find a providing package name. + * + * @return string + * The name of the package that provided the scaffold file information. + */ + protected function findProvidingPackage(array $list_of_scaffold_files, ScaffoldFileInfo $scaffold_file) { + // The scaffold file should always be in our list, but we will check + // just to be sure that it really is. + $dest_rel_path = $scaffold_file->destination()->relativePath(); + if (!array_key_exists($dest_rel_path, $list_of_scaffold_files)) { + throw new \RuntimeException("Scaffold file not found in list of all scaffold files."); + } + return $list_of_scaffold_files[$dest_rel_path]->packageName(); + } + + /** + * Process all of the scaffold files listed in the provided file mappings. + * + * @param array $file_mappings + * An multidimensional array of file mappings, as returned by + * self::getFileMappingsFromPackages(). + * @param \Drupal\Component\Scaffold\Interpolator $location_replacements + * An object with the location mappings (e.g. [web-root]). + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the top-level composer.json file. + * + * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[] + * Associative array keyed by destination path and values as the scaffold + * result for each scaffolded file. + */ + public function process(array $file_mappings, Interpolator $location_replacements, ScaffoldOptions $options) { + list($list_of_scaffold_files, $resolved_file_mappings) = $this->collateScaffoldFiles($file_mappings, $location_replacements); + return $this->processScaffoldFiles($list_of_scaffold_files, $resolved_file_mappings, $options); + } + + /** + * Organizes provided file mappings by destination and package. + * + * @param array $file_mappings + * An multidimensional array of file mappings, as returned by + * self::getFileMappingsFromPackages(). + * @param \Drupal\Component\Scaffold\Interpolator $location_replacements + * An object with the location mappings (e.g. [web-root]). + * + * @return array + * A list containing two lists: + * - Associative array containing destination => operation mappings. + * - Associative array containing package name => file mappings. + */ + protected function collateScaffoldFiles(array $file_mappings, Interpolator $location_replacements) { + $resolved_file_mappings = []; + /** @var \Drupal\Component\Scaffold\ScaffoldFileInfo[] $list_of_scaffold_files */ + $list_of_scaffold_files = []; + foreach ($file_mappings as $package_name => $package_file_mappings) { + foreach ($package_file_mappings as $destination_rel_path => $op) { + $destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements); + // If there was already a scaffolding operation happening at this path, + // and the new operation is Conjoinable, then use a ConjunctionOp to + // join together both operations. This will cause both operations to + // run, one after the other. At the moment, only AppendOp is conjoinable; + // all other operations simply replace anything at the same path. + if (isset($list_of_scaffold_files[$destination_rel_path]) && $op instanceof ConjoinableInterface) { + $op = new ConjunctionOp($list_of_scaffold_files[$destination_rel_path]->op(), $op); + } + + $scaffold_file = new ScaffoldFileInfo($destination, $op); + $list_of_scaffold_files[$destination_rel_path] = $scaffold_file; + $resolved_file_mappings[$package_name][$destination_rel_path] = $scaffold_file; + } + } + return [$list_of_scaffold_files, $resolved_file_mappings]; + } + + /** + * Scaffolds the files in our scaffold collection, package-by-package. + * + * @param array $list_of_scaffold_files + * Associative array containing destination => operation mappings. + * @param array $resolved_file_mappings + * Associative array containing package name => file mappings. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the top-level composer.json file. + * + * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[] + * Associative array keyed by destination path and values as the scaffold + * result for each scaffolded file. + */ + protected function processScaffoldFiles(array $list_of_scaffold_files, array $resolved_file_mappings, ScaffoldOptions $options) { + $result = []; + // 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 = $this->findProvidingPackage($list_of_scaffold_files, $scaffold_file); + if ($scaffold_file->overridden($overriding_package)) { + $this->io->write($scaffold_file->interpolate(" - Skip [dest-rel-path]: overridden in {$overriding_package}")); + } + else { + $result[$dest_rel_path] = $scaffold_file->process($this->io, $options); + } + } + } + return $result; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php b/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php new file mode 100644 index 0000000000..7ede3241d6 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php @@ -0,0 +1,185 @@ +composer = $composer; + } + + /** + * Creates a scaffolding operation object as determined by the metadata. + * + * @param \Composer\Package\PackageInterface $package + * The package that relative paths will be relative from. + * @param string $destination + * The destination path for the scaffold file. Used only for error messages. + * @param mixed $metadata + * The metadata for this operation object, which varies by operation type. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * The scaffolding operation object (skip, replace, etc.) + * + * @throws \RuntimeException + * Exception thrown when $metadata can not be used to determine a scaffold + * operation. + */ + public function create(PackageInterface $package, $destination, $metadata) { + $metadata = $this->normalizeScaffoldMetadata($destination, $metadata); + switch ($metadata['mode']) { + case 'skip': + return new SkipOp(); + + case 'replace': + return $this->createReplaceOp($package, $destination, $metadata); + + case 'append': + return $this->createAppendOp($package, $destination, $metadata); + } + throw new \RuntimeException("Unknown scaffold operation mode {$metadata['mode']}."); + } + + /** + * Creates a 'replace' scaffold op. + * + * Replace ops may copy or symlink, depending on settings. + * + * @param \Composer\Package\PackageInterface $package + * The package that relative paths will be relative from. + * @param string $destination + * The destination path for the scaffold file. Used only for error messages. + * @param array $metadata + * The metadata for this operation object, i.e. the relative 'path'. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * A scaffold replace operation object. + */ + protected function createReplaceOp(PackageInterface $package, $destination, array $metadata) { + // If this op does not provide an 'overwrite' value, default it to true. + $metadata += ['overwrite' => TRUE]; + if (!isset($metadata['path'])) { + throw new \RuntimeException("'path' component required for 'replace' operations."); + } + $package_name = $package->getName(); + $package_path = $this->getPackagePath($package); + $source = ScaffoldFilePath::sourcePath($package_name, $package_path, $destination, $metadata['path']); + $op = new ReplaceOp($source, $metadata['overwrite']); + return $op; + } + + /** + * Creates an 'append' (or 'prepend') scaffold op. + * + * @param \Composer\Package\PackageInterface $package + * The package that relative paths will be relative from. + * @param string $destination + * The destination path for the scaffold file. Used only for error messages. + * @param array $metadata + * The metadata for this operation object, i.e. the relative 'path'. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * A scaffold replace operation object. + */ + protected function createAppendOp(PackageInterface $package, $destination, array $metadata) { + $package_name = $package->getName(); + $package_path = $this->getPackagePath($package); + $prepend_source_file = NULL; + $append_source_file = NULL; + if (isset($metadata['prepend'])) { + $prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $destination, $metadata['prepend']); + } + if (isset($metadata['append'])) { + $append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $destination, $metadata['append']); + } + $op = new AppendOp($prepend_source_file, $append_source_file); + return $op; + } + + /** + * Gets the file path of a package. + * + * Note that if we call getInstallPath on the root package, we get the + * wrong answer (the installation manager thinks our package is in + * vendor). We therefore add special checking for this case. + * + * @param \Composer\Package\PackageInterface $package + * The package. + * + * @return string + * The file path. + */ + protected function getPackagePath(PackageInterface $package) { + if ($package->getName() == $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($package); + } + } + + /** + * Normalizes metadata by converting literal values into arrays. + * + * Conversions performed include: + * - Boolean 'false' means "skip". + * - A string means "replace", with the string value becoming the path. + * + * @param string $destination + * The destination path for the scaffold file. + * @param mixed $value + * The metadata for this operation object, which varies by operation type. + * + * @return array + * Normalized scaffold metadata. + */ + protected function normalizeScaffoldMetadata($destination, $value) { + if (is_bool($value)) { + if (!$value) { + return ['mode' => 'skip']; + } + throw new \RuntimeException("File mapping {$destination} cannot be given the value 'true'."); + } + if (empty($value)) { + throw new \RuntimeException("File mapping {$destination} cannot be empty."); + } + if (is_string($value)) { + $value = ['path' => $value]; + } + // If there is no 'mode', but there is an 'append' or a 'prepend' path, + // then the mode is 'append' (append + prepend). + if (!isset($value['mode']) && (isset($value['append']) || isset($value['prepend']))) { + $value['mode'] = 'append'; + } + // If there is no 'mode', then the default is 'replace'. + if (!isset($value['mode'])) { + $value['mode'] = 'replace'; + } + return $value; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php b/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php new file mode 100644 index 0000000000..8fa0cee2be --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php @@ -0,0 +1,29 @@ +source = $sourcePath; + $this->overwrite = $overwrite; + } + + /** + * Copy or Symlink the specified scaffold file. + * + * {@inheritdoc} + */ + public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { + $fs = new Filesystem(); + $destination_path = $destination->fullPath(); + // Do nothing if overwrite is 'false' and a file already exists at the + // destination. + if ($this->overwrite === FALSE && file_exists($destination_path)) { + $interpolator = $destination->getInterpolator(); + $io->write($interpolator->interpolate(" - Skip [dest-rel-path] because it already exists and overwrite is false.")); + return new ScaffoldResult($destination, FALSE); + } + + // 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)); + if ($options->symlink() == TRUE) { + return $this->symlinkScaffold($destination, $io); + } + return $this->copyScaffold($destination, $io); + } + + /** + * Copies the scaffold file. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination + * Scaffold file to process. + * @param \Composer\IO\IOInterface $io + * IOInterface to writing to. + * + * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult + * The scaffold result. + */ + protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) { + $interpolator = $destination->getInterpolator(); + $this->source->addInterpolationData($interpolator); + $success = copy($this->source->fullPath(), $destination->fullPath()); + if (!$success) { + throw new \RuntimeException($interpolator->interpolate("Could not copy source file [src-rel-path] to [dest-rel-path]!")); + } + $io->write($interpolator->interpolate(" - Copy [dest-rel-path] from [src-rel-path]")); + return new ScaffoldResult($destination, $this->overwrite); + } + + /** + * Symlinks the scaffold file. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination + * Scaffold file to process. + * @param \Composer\IO\IOInterface $io + * IOInterface to writing to. + * + * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult + * The scaffold result. + */ + protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) { + $interpolator = $destination->getInterpolator(); + try { + $fs = new Filesystem(); + $fs->relativeSymlink($this->source->fullPath(), $destination->fullPath()); + } + catch (\Exception $e) { + throw new \RuntimeException($interpolator->interpolate("Could not symlink source file [src-rel-path] to [dest-rel-path]!"), [], $e); + } + $io->write($interpolator->interpolate(" - Link [dest-rel-path] from [src-rel-path]")); + return new ScaffoldResult($destination, $this->overwrite); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php new file mode 100644 index 0000000000..b542b1e1a3 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php @@ -0,0 +1,59 @@ +destination = $destination; + $this->managed = $isManaged; + } + + /** + * Determines whether this scaffold file is managed. + * + * @return bool + * TRUE if this scaffold file is managed, FALSE if not. + */ + public function isManaged() { + return $this->managed; + } + + /** + * Gets the destination scaffold file that this result refers to. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * The destination path for the scaffold result. + */ + public function destination() { + return $this->destination; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php new file mode 100644 index 0000000000..444dafac7e --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php @@ -0,0 +1,23 @@ +getInterpolator(); + $io->write($interpolator->interpolate(" - Skip [dest-rel-path]: disabled")); + return new ScaffoldResult($destination, FALSE); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Plugin.php b/core/lib/Drupal/Component/Scaffold/Plugin.php new file mode 100644 index 0000000000..cee41d8417 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Plugin.php @@ -0,0 +1,90 @@ +handler = new Handler($composer, $io); + } + + /** + * {@inheritdoc} + */ + public function getCapabilities() { + return ['Composer\Plugin\Capability\CommandProvider' => ScaffoldCommandProvider::class]; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + ScriptEvents::POST_UPDATE_CMD => 'postCmd', + PackageEvents::POST_PACKAGE_INSTALL => 'postPackage', + PluginEvents::COMMAND => 'onCommand', + ]; + } + + /** + * Post command event callback. + * + * @param \Composer\Script\Event $event + * The Composer event. + */ + public function postCmd(Event $event) { + $this->handler->scaffold(); + } + + /** + * 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); + } + + /** + * Pre command event callback. + * + * @param \Composer\Plugin\CommandEvent $event + * The Composer command event. + */ + public function onCommand(CommandEvent $event) { + if ($event->getCommandName() == 'require') { + $this->handler->beforeRequire($event); + } + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php b/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php new file mode 100644 index 0000000000..2285146f27 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php @@ -0,0 +1,22 @@ +destination = $destination; + $this->op = $op; + } + + /** + * Gets the Scaffold operation. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * Operations object that handles scaffolding (copy, make symlink, etc). + */ + public function op() { + return $this->op; + } + + /** + * Gets the package name. + * + * @return string + * The name of the package this scaffold file info was collected from. + */ + public function packageName() { + return $this->destination->packageName(); + } + + /** + * Gets the destination. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * The scaffold path to the destination file. + */ + public function destination() { + return $this->destination; + } + + /** + * Determines 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($providing_package) { + return $this->packageName() !== $providing_package; + } + + /** + * Replaces placeholders in a message. + * + * @param string $message + * Message with placeholders to fill in. + * @param array $extra + * Additional data to merge with the interpolator. + * @param mixed $default + * Default value to use for missing placeholders, or FALSE to keep them. + * + * @return string + * Interpolated string with placeholders replaced. + */ + public function interpolate($message, array $extra = [], $default = FALSE) { + $interpolator = $this->getInterpolator(); + return $interpolator->interpolate($message, $extra, $default); + } + + /** + * Moves a single scaffold file from source to destination. + * + * @param \Composer\IO\IOInterface $io + * The scaffold file to be processed. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Assorted operational options, e.g. whether the destination should be a + * symlink. + * + * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult + * The scaffold result. + */ + public function process(IOInterface $io, ScaffoldOptions $options) { + return $this->op()->process($this->destination, $io, $options); + } + + /** + * Interpolates a string using the data from this scaffold file info. + * + * @return \Drupal\Component\Scaffold\Interpolator + * An interpolator for making string replacements. + */ + protected function getInterpolator() { + return $this->destination->getInterpolator(); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php b/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php new file mode 100644 index 0000000000..8e842a68e1 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php @@ -0,0 +1,190 @@ +type = $path_type; + $this->packageName = $package_name; + $this->relativePath = $rel_path; + $this->fullPath = $full_path; + } + + /** + * Gets the name of the package this source file was pulled from. + * + * @return string + * Name of package. + */ + public function packageName() { + return $this->packageName; + } + + /** + * Gets the relative path to the source file (best to use in messages). + * + * @return string + * Relative path to file. + */ + public function relativePath() { + return $this->relativePath; + } + + /** + * Gets the full path to the source file. + * + * @return string + * Full path to file. + */ + public function fullPath() { + return $this->fullPath; + } + + /** + * Converts the relative source path into an absolute path. + * + * The path returned will be relative to the package installation location. + * + * @param string $package_name + * The name of the package containing the source file. Only used for error + * messages. + * @param string $package_path + * The installation path of the package containing the source file. + * @param string $destination + * Destination location provided as a relative path. Only used for error + * messages. + * @param string $source + * Source location provided as a relative path. + * + * @return self + * Object wrapping the relative and absolute path to the source file. + */ + public static function sourcePath($package_name, $package_path, $destination, $source) { + // Complain if there is no source path. + if (empty($source)) { + throw new \RuntimeException("No scaffold file path given for {$destination} in package {$package_name}."); + } + // Calculate the full path to the source scaffold file. + $source_full_path = $package_path . '/' . $source; + if (!file_exists($source_full_path)) { + throw new \RuntimeException("Scaffold file {$source} not found in package {$package_name}."); + } + if (is_dir($source_full_path)) { + throw new \RuntimeException("Scaffold file {$source} in package {$package_name} is a directory; only files may be scaffolded."); + } + return new self('src', $package_name, $source, $source_full_path); + } + + /** + * Converts the relative destination path into an absolute path. + * + * Any placeholders in the destination path, e.g. '[web-root]', will be + * replaced using the provided location replacements interpolator. + * + * @param string $package_name + * The name of the package defining the destination path. + * @param string $destination + * The relative path to the destination file being scaffolded. + * @param \Drupal\Component\Scaffold\Interpolator $location_replacements + * Interpolator that includes the [web-root] and any other available + * placeholder replacements. + * + * @return self + * Object wrapping the relative and absolute path to the destination file. + */ + public static function destinationPath($package_name, $destination, Interpolator $location_replacements) { + $dest_full_path = $location_replacements->interpolate($destination); + return new self('dest', $package_name, $destination, $dest_full_path); + } + + /** + * Adds data about the relative and full path to the provided interpolator. + * + * @param \Drupal\Component\Scaffold\Interpolator $interpolator + * Interpolator to add data to. + * @param string $name_prefix + * (optional) Prefix to add before -rel-path and -full-path item names. + * Defaults to path type provided when constructing this object. + */ + public function addInterpolationData(Interpolator $interpolator, $name_prefix = '') { + if (empty($name_prefix)) { + $name_prefix = $this->type; + } + $data = [ + 'package-name' => $this->packageName(), + "{$name_prefix}-rel-path" => $this->relativePath(), + "{$name_prefix}-full-path" => $this->fullPath(), + ]; + $interpolator->addData($data); + } + + /** + * Interpolate a string using the data from this scaffold file info. + * + * @param string $name_prefix + * (optional) Prefix to add before -rel-path and -full-path item names. + * Defaults to path type provided when constructing this object. + * + * @return \Drupal\Component\Scaffold\Interpolator + * An interpolator for making string replacements. + */ + public function getInterpolator($name_prefix = '') { + $interpolator = new Interpolator(); + $this->addInterpolationData($interpolator, $name_prefix); + return $interpolator; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php b/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php new file mode 100644 index 0000000000..ddc86659a0 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php @@ -0,0 +1,197 @@ +options = $options + [ + "allowed-packages" => [], + "locations" => [], + "symlink" => FALSE, + "file-mapping" => [], + ]; + } + + /** + * Determines if the provided 'extras' section has scaffold options. + * + * @param array $extras + * The contents of the 'extras' section. + * + * @return bool + * True if scaffold options have been declared + */ + public static function hasOptions(array $extras) { + return array_key_exists('composer-scaffold', $extras); + } + + /** + * Creates a scaffold options object. + * + * @param array $extras + * The contents of the 'extras' section. + * + * @return self + * The scaffold options object representing the provided scaffold options + */ + public static function create(array $extras) { + $options = static::hasOptions($extras) ? $extras['composer-scaffold'] : []; + return new self($options); + } + + /** + * Creates a new scaffold options object with some values overridden. + * + * @param array $options + * Override values. + * + * @return self + * The scaffold options object representing the provided scaffold options + */ + protected function override(array $options) { + return new self($options + $this->options); + } + + /** + * Creates a new scaffold options object with an overridden 'symlink' value. + * + * @param bool $symlink + * Whether symlinking should be enabled or not. + * + * @return self + * The scaffold options object representing the provided scaffold options + */ + public function overrideSymlink($symlink) { + return $this->override(['symlink' => $symlink]); + } + + /** + * Determines whether any allowed packages were defined. + * + * @return bool + * Whether there are allowed packages + */ + public function hasAllowedPackages() { + return !empty($this->allowedPackages()); + } + + /** + * Gets allowed packages from these options. + * + * @return array + * The list of allowed packages + */ + public function allowedPackages() { + return $this->options['allowed-packages']; + } + + /** + * Gets the location mapping table, e.g. 'webroot' => './'. + * + * @return array + * A map of name : location values + */ + public function locations() { + return $this->options['locations']; + } + + /** + * Determines whether a given named location is defined. + * + * @param string $name + * The location name to search for. + * + * @return bool + * True if the specified named location exist. + */ + protected function hasLocation($name) { + return array_key_exists($name, $this->locations()); + } + + /** + * Gets a specific named location. + * + * @param string $name + * The name of the location to fetch. + * @param string $default + * The value to return if the requested location is not defined. + * + * @return string + * The value of the provided named location + */ + public function getLocation($name, $default = '') { + return $this->hasLocation($name) ? $this->locations()[$name] : $default; + } + + /** + * Determines if symlink mode is set. + * + * @return bool + * Whether or not 'symlink' mode + */ + public function symlink() { + return $this->options['symlink']; + } + + /** + * Determines if there are file mappings. + * + * @return bool + * Whether or not the scaffold options contain any file mappings + */ + public function hasFileMapping() { + return !empty($this->fileMapping()); + } + + /** + * Returns the actual file mappings. + * + * @return array + * File mappings for just this config type. + */ + public function fileMapping() { + return $this->options['file-mapping']; + } + + /** + * Determines if there is defined a value for the 'gitignore' option. + * + * @return bool + * Whether or not there is a 'gitignore' option setting + */ + public function hasGitIgnore() { + return isset($this->options['gitignore']); + } + + /** + * Gets the value of the 'gitignore' option. + * + * @return bool + * The 'gitignore' option, or TRUE if undefined. + */ + public function gitIgnore() { + return $this->hasGitIgnore() ? $this->options['gitignore'] : TRUE; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/TESTING.txt b/core/lib/Drupal/Component/Scaffold/TESTING.txt new file mode 100644 index 0000000000..3186f0f901 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/TESTING.txt @@ -0,0 +1,18 @@ +HOW-TO: Test this Drupal component + +In order to test this component, you'll need to get the entire Drupal repo and +run the tests there. + +You'll find the tests under core/tests/Drupal/Tests/Component. + +You can get the full Drupal repo here: +https://www.drupal.org/project/drupal/git-instructions + +You can find more information about running PHPUnit tests with Drupal here: +https://www.drupal.org/node/2116263 + +Each component in the Drupal\Component namespace has its own annotated test +group. You can use this group to run only the tests for this component. Like +this: + +$ ./vendor/bin/phpunit -c core --group Scaffold diff --git a/core/lib/Drupal/Component/Scaffold/composer.json b/core/lib/Drupal/Component/Scaffold/composer.json new file mode 100644 index 0000000000..e826f21d36 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/composer.json @@ -0,0 +1,29 @@ +{ + "name": "drupal/core-composer-scaffold", + "description": "A flexible Composer project scaffold builder.", + "type": "composer-plugin", + "keywords": ["drupal"], + "homepage": "https://www.drupal.org/project/drupal", + "license": "GPL-2.0-or-later", + "require": { + "composer-plugin-api": "^1.0.0", + "php": ">=7.0.8" + }, + "autoload": { + "psr-4": { + "Drupal\\Component\\Scaffold\\": "" + } + }, + "extra": { + "class": "Drupal\\Component\\Scaffold\\Plugin", + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "config": { + "sort-packages": true + }, + "require-dev": { + "composer/composer": "^1.8@stable" + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php b/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php new file mode 100644 index 0000000000..3fdb950e36 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php @@ -0,0 +1,27 @@ +assertFileExists($path); + $contents = file_get_contents($path); + $this->assertRegExp($contents_contains, basename($path) . ': ' . $contents); + $this->assertSame($is_link, is_link($path)); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/ExecTrait.php b/core/tests/Drupal/Tests/Component/Scaffold/ExecTrait.php new file mode 100644 index 0000000000..0986961788 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/ExecTrait.php @@ -0,0 +1,36 @@ + getenv('PATH'), 'HOME' => getenv('HOME')]); + $process->inheritEnvironmentVariables(); + $process->setTimeout(300)->setIdleTimeout(300)->run(); + $exitCode = $process->getExitCode(); + if (0 != $exitCode) { + throw new \RuntimeException("Exit code: {$exitCode}\n\n" . $process->getErrorOutput() . "\n\n" . $process->getOutput()); + } + return [$process->getOutput(), $process->getErrorOutput()]; + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php b/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php new file mode 100644 index 0000000000..de91e9381b --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php @@ -0,0 +1,368 @@ +io) { + $this->io = new BufferIO(); + } + return $this->io; + } + + /** + * Gets the Composer object. + * + * @return \Composer\Composer + * The main Composer object, needed by the scaffold Handler, etc. + */ + public function getComposer() { + if (!$this->composer) { + $this->composer = Factory::create($this->io(), NULL, TRUE); + } + return $this->composer; + } + + /** + * Gets the output from the io() fixture. + * + * @return string + * Output captured from tests that write to Fixtures::io(). + */ + public function getOutput() { + return $this->io()->getOutput(); + } + + /** + * Gets the path to Scaffold component. + * + * Used to inject the component into composer.json files. + * + * @return string + * Path to the root of this project. + */ + public function projectRoot() { + return realpath(__DIR__) . '/../../../../../../core/lib/Drupal/Component/Scaffold'; + } + + /** + * Gets the path to the project fixtures. + * + * @return string + * Path to project fixtures + */ + public function allFixturesDir() { + return realpath(__DIR__ . '/fixtures'); + } + + /** + * Gets the path to one particular project fixture. + * + * @param string $project_name + * The project name to get the path for. + * + * @return string + * Path to project fixture. + */ + public function projectFixtureDir($project_name) { + $dir = $this->allFixturesDir() . '/' . $project_name; + if (!is_dir($dir)) { + throw new \RuntimeException("Requested fixture project {$project_name} that does not exist."); + } + return $dir; + } + + /** + * Gets the path to one particular bin path. + * + * @param string $bin_name + * The bin name to get the path for. + * + * @return string + * Path to project fixture. + */ + public function binFixtureDir($bin_name) { + $dir = $this->allFixturesDir() . '/scripts/' . $bin_name; + if (!is_dir($dir)) { + throw new \RuntimeException("Requested fixture bin dir {$bin_name} that does not exist."); + } + return $dir; + } + + /** + * Gets a path to a source scaffold fixture. + * + * Use in place of ScaffoldFilePath::sourcePath(). + * + * @param string $project_name + * The name of the project to fetch; $package_name is + * "fixtures/$project_name". + * @param string $source + * The name of the asset; path is "assets/$source". + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * The full and relative path to the desired asset + * + * @see \Drupal\Component\Scaffold\ScaffoldFilePath::sourcePath() + */ + public function sourcePath($project_name, $source) { + $package_name = "fixtures/{$project_name}"; + $source_rel_path = "assets/{$source}"; + $package_path = $this->projectFixtureDir($project_name); + return ScaffoldFilePath::sourcePath($package_name, $package_path, 'unknown', $source_rel_path); + } + + /** + * Gets an Interpolator with 'web-root' and 'package-name' set. + * + * Use in place of ManageOptions::getLocationReplacements(). + * + * @return \Drupal\Component\Scaffold\Interpolator + * An interpolator with location replacements, including 'web-root'. + * + * @see \Drupal\Component\Scaffold\ManageOptions::getLocationReplacements() + */ + public function getLocationReplacements() { + $destinationTmpDir = $this->mkTmpDir(); + $interpolator = new Interpolator(); + $interpolator->setData(['web-root' => $destinationTmpDir, 'package-name' => 'fixtures/tmp-destination']); + return $interpolator; + } + + /** + * Creates a ReplaceOp fixture. + * + * @param string $project_name + * The name of the project to fetch; $package_name is + * "fixtures/$project_name". + * @param string $source + * The name of the asset; path is "assets/$source". + * + * @return \Drupal\Component\Scaffold\Operations\ReplaceOp + * A replace operation object. + */ + public function replaceOp($project_name, $source) { + $source_path = $this->sourcePath($project_name, $source); + return new ReplaceOp($source_path, TRUE); + } + + /** + * Creates an AppendOp fixture. + * + * @param string $project_name + * The name of the project to fetch; $package_name is + * "fixtures/$project_name". + * @param string $source + * The name of the asset; path is "assets/$source". + * + * @return \Drupal\Component\Scaffold\Operations\AppendOp + * An append operation object. + */ + public function appendOp($project_name, $source) { + $source_path = $this->sourcePath($project_name, $source); + return new AppendOp(NULL, $source_path); + } + + /** + * Gets a destination path in a tmp dir. + * + * Use in place of ScaffoldFilePath::destinationPath(). + * + * @param string $destination + * Destination path; should be in the form '[web-root]/robots.txt', where + * '[web-root]' is always literally '[web-root]', with any arbitrarily + * desired filename following. + * @param \Drupal\Component\Scaffold\Interpolator $interpolator + * Location replacements. Obtain via Fixtures::getLocationReplacements() + * when creating multiple scaffold destinations. + * @param string $package_name + * (optional) The name of the fixture package that this path came from. + * Taken from interpolator if not provided. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * A destination scaffold file backed by temporary storage. + * + * @see \Drupal\Component\Scaffold\ScaffoldFilePath::destinationPath() + */ + public function destinationPath($destination, Interpolator $interpolator = NULL, $package_name = NULL) { + $interpolator = $interpolator ?: $this->getLocationReplacements(); + $package_name = $package_name ?: $interpolator->interpolate('[package-name]'); + return ScaffoldFilePath::destinationPath($package_name, $destination, $interpolator); + } + + /** + * Generates a path to a temporary location, but do not create the directory. + * + * @param string $extraSalt + * Extra characters to throw into the md5 to add to name. + * + * @return string + * Path to temporary directory + */ + public function tmpDir($extraSalt = '') { + $tmpDir = sys_get_temp_dir() . '/composer-scaffold-test-' . md5($extraSalt . microtime()); + $this->tmpDirs[] = $tmpDir; + return $tmpDir; + } + + /** + * Creates a temporary directory. + * + * @param string $extraSalt + * Extra characters to throw into the md5 to add to name. + * + * @return string + * Path to temporary directory + */ + public function mkTmpDir($extraSalt = '') { + $tmpDir = $this->tmpDir($extraSalt); + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($tmpDir); + return $tmpDir; + } + + /** + * Calls 'tearDown' in any test that copies fixtures to transient locations. + */ + public function tearDown() { + // Remove any temporary directories that were created. + $filesystem = new Filesystem(); + foreach ($this->tmpDirs as $dir) { + $filesystem->remove($dir); + } + // Clear out variables from the previous pass. + $this->tmpDirs = []; + $this->io = NULL; + } + + /** + * Creates a temporary copy of all of the fixtures projects into a temp dir. + * + * The fixtures remain dirty if they already exist. Individual tests should + * first delete any fixture directory that needs to remain pristine. Since all + * temporary directories are removed in tearDown, this is only an issue when + * a) the FIXTURE_DIR environment variable has been set, or b) tests are + * calling cloneFixtureProjects more than once per test method. + * + * @param string $fixturesDir + * The directory to place fixtures in. + * @param array $replacements + * Key : value mappings for placeholders to replace in composer.json + * templates. + */ + public function cloneFixtureProjects($fixturesDir, array $replacements = []) { + $filesystem = new Filesystem(); + $replacements += ['SYMLINK' => 'true']; + $interpolator = new Interpolator('__', '__'); + $interpolator->setData($replacements); + $filesystem->copy($this->allFixturesDir(), $fixturesDir); + $composer_json_templates = glob($fixturesDir . "/*/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); + } + } + } + + /** + * Runs the scaffold operation. + * + * This is equivalent to running `composer composer-scaffold`, but we do the + * equivalent operation by instantiating a Handler object in order to continue + * running in the same process, so that coverage may be calculated for the + * code executed by these tests. + * + * @param string $cwd + * The working directory to run the scaffold command in. + * + * @return string + * Output captured from tests that write to Fixtures::io(). + */ + public function runScaffold($cwd) { + chdir($cwd); + $handler = new Handler($this->getComposer(), $this->io()); + $handler->scaffold(); + return $this->getOutput(); + } + + /** + * Runs a `composer` command. + * + * @param string $cmd + * The Composer command to execute (escaped as required) + * @param string $cwd + * The current working directory to run the command from. + * @param int $expectedExitCode + * The expected exit code; will throw if a different exit code is returned. + * + * @return string + * Standard output and standard error from the command. + */ + public function runComposer($cmd, $cwd, $expectedExitCode = 0) { + chdir($cwd); + $input = new StringInput($cmd); + $output = new BufferedOutput(); + $application = new Application(); + $application->setAutoExit(FALSE); + try { + $exitCode = $application->run($input, $output); + if ($exitCode != $expectedExitCode) { + print "Command '{$cmd}' - Expected exit code: {$expectedExitCode}, actual exit code: {$exitCode}\n"; + } + } + catch (\Exception $e) { + print "Exception: " . $e->getMessage() . "\n"; + } + $output = $output->fetch(); + return $output; + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php new file mode 100644 index 0000000000..c48d8688c7 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php @@ -0,0 +1,147 @@ +fileSystem = new Filesystem(); + $this->fixtures = new Fixtures(); + $this->projectRoot = $this->fixtures->projectRoot(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + // Remove any temporary directories et. al. that were created. + $this->fixtures->tearDown(); + } + + /** + * Test to see if scaffold operation runs at the correct times. + */ + public function testComposerHooks() { + $this->fixturesDir = $this->fixtures->tmpDir($this->getName()); + $is_link = FALSE; + $replacements = ['SYMLINK' => $is_link ? 'true' : 'false', 'PROJECT_ROOT' => $this->projectRoot]; + $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements); + $topLevelProjectDir = 'composer-hooks-fixture'; + $sut = $this->fixturesDir . '/' . $topLevelProjectDir; + // First test: run composer install. This is the same as composer update + // since there is no lock file. Ensure that scaffold operation ran. + $this->execComposer("install --no-ansi", $sut); + $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#Test version of default.settings.php from drupal/core#'); + // Run composer required to add in the scaffold-override-fixture. This + // project is "allowed" in our main fixture project, but not required. + // We expect that requiring this library should re-scaffold, resulting + // in a changed default.settings.php file. + list($stdout,) = $this->execComposer("require --no-ansi --no-interaction fixtures/scaffold-override-fixture:dev-master", $sut); + $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#'); + // Make sure that the appropriate notice informing us that scaffolding + // is allowed was printed. + $this->assertContains('Package fixtures/scaffold-override-fixture has scaffold operations, and is already allowed in the root-level composer.json file.', $stdout); + // Delete one scaffold file, just for test purposes, then run + // 'composer update' and see if the scaffold file is replaced. + @unlink($sut . '/sites/default/default.settings.php'); + $this->execComposer("update --no-ansi", $sut); + $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#'); + // Delete the same test scaffold file again, then run + // 'composer composer:scaffold' and see if the scaffold file is replaced. + @unlink($sut . '/sites/default/default.settings.php'); + $this->execComposer("composer:scaffold --no-ansi", $sut); + $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#'); + // Run 'composer create-project' to create a new test project called + // 'create-project-test', which is a copy of 'fixtures/drupal-drupal'. + $sut = $this->fixturesDir . '/create-project-test'; + $filesystem = new Filesystem(); + $filesystem->remove($sut); + list($stdout,) = $this->execComposer("create-project --repository=packages.json fixtures/drupal-drupal {$sut}", $this->fixturesDir, ['COMPOSER_MIRROR_PATH_REPOS' => 1]); + $this->assertDirectoryExists($sut); + $this->assertContains('Scaffolding files for fixtures/drupal-drupal', $stdout); + $this->assertScaffoldedFile($sut . '/index.php', FALSE, '#Test version of index.php from drupal/core#'); + $topLevelProjectDir = 'composer-hooks-nothing-allowed-fixture'; + $sut = $this->fixturesDir . '/' . $topLevelProjectDir; + // Run composer install on an empty project. + $this->execComposer("install --no-ansi", $sut); + // Require a project that is not allowed to scaffold and confirm that we + // get a warning, and it does not scaffold. + list($stdout,) = $this->execComposer("require --no-ansi --no-interaction fixtures/scaffold-override-fixture:dev-master", $sut); + $this->assertFileNotExists($sut . '/sites/default/default.settings.php'); + $this->assertContains('Package fixtures/scaffold-override-fixture has scaffold operations, but it is not allowed in the root-level composer.json file.', $stdout); + } + + /** + * Runs a `composer` command. + * + * @param string $cmd + * The Composer command to execute (escaped as required) + * @param string $cwd + * The current working directory to run the command from. + * @param array $env + * Environment variables to define for the subprocess. + * + * @return array + * Standard output and standard error from the command + */ + protected function execComposer($cmd, $cwd, array $env = []) { + return $this->mustExec("composer {$cmd}", $cwd, $env); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php new file mode 100644 index 0000000000..fc6f63f8c7 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php @@ -0,0 +1,191 @@ +fileSystem = new Filesystem(); + $this->fixtures = new Fixtures(); + $this->projectRoot = $this->fixtures->projectRoot(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + // Remove any temporary directories et. al. that were created. + $this->fixtures->tearDown(); + } + + /** + * Creates a system-under-test and initialize a git repository for it. + * + * @param string $fixture_name + * The name of the fixture to use from + * core/tests/Drupal/Tests/Component/Scaffold/fixtures. + * + * @return string + * The path to the fixture directory. + */ + protected function createSutWithGit($fixture_name) { + $is_link = FALSE; + $this->fixturesDir = $this->fixtures->tmpDir($this->getName()); + $sut = $this->fixturesDir . '/' . $fixture_name; + $replacements = ['SYMLINK' => $is_link ? 'true' : 'false', 'PROJECT_ROOT' => $this->projectRoot]; + $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements); + // .gitignore files will not be managed unless there is a git repository. + $this->mustExec('git init', $sut); + // Add some user info so git does not complain. + $this->mustExec('git config user.email "test@example.com"', $sut); + $this->mustExec('git config user.name "Test User"', $sut); + $this->mustExec('git add .', $sut); + $this->mustExec('git commit -m "Initial commit."', $sut); + // Run composer install, but suppress scaffolding. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $sut); + return $sut; + } + + /** + * Tests scaffold command correctly manages the .gitignore file. + */ + public function testManageGitIgnore() { + // Note that the drupal-composer-drupal-project fixture does not + // have any configuration settings related to .gitignore management. + $sut = $this->createSutWithGit('drupal-composer-drupal-project'); + $this->assertFileNotExists($sut . '/docroot/index.php'); + $this->assertFileNotExists($sut . '/docroot/sites/.gitignore'); + // Run the scaffold command. + $this->fixtures->runScaffold($sut); + $this->assertFileExists($sut . '/docroot/index.php'); + $expected = <<assertScaffoldedFile($sut . '/docroot/.gitignore', FALSE, '#' . $expected . '#msi'); + $this->assertScaffoldedFile($sut . '/docroot/sites/.gitignore', FALSE, '#example.settings.local.php#'); + $this->assertScaffoldedFile($sut . '/docroot/sites/default/.gitignore', FALSE, '#default.services.yml#'); + $expected = <<mustExec('git status --porcelain', $sut); + $this->assertEquals(trim($expected), trim($stdout)); + } + + /** + * Tests scaffold command does not manage the .gitignore file when disabled. + */ + public function testUnmanagedGitIgnoreWhenDisabled() { + // Note that the drupal-drupal fixture has a configuration setting + // `"gitignore": false,` which disables .gitignore file handling. + $sut = $this->createSutWithGit('drupal-drupal'); + $this->assertFileNotExists($sut . '/docroot/index.php'); + // Run the scaffold command. + $this->fixtures->runScaffold($sut); + $this->assertFileExists($sut . '/index.php'); + $this->assertFileNotExists($sut . '/.gitignore'); + $this->assertFileNotExists($sut . '/docroot/sites/default/.gitignore'); + } + + /** + * Tests scaffold command disables .gitignore management when git not present. + * + * The scaffold operation should still succeed if there is no 'git' + * executable. + */ + public function testUnmanagedGitIgnoreWhenGitNotAvailable() { + // Note that the drupal-composer-drupal-project fixture does not have any + // configuration settings related to .gitignore management. + $sut = $this->createSutWithGit('drupal-composer-drupal-project'); + $this->assertFileNotExists($sut . '/docroot/sites/default/.gitignore'); + $this->assertFileNotExists($sut . '/docroot/index.php'); + $this->assertFileNotExists($sut . '/docroot/sites/.gitignore'); + // Confirm that 'git' is available (n.b. if it were not, createSutWithGit() + // would fail). + exec('git --help', $output, $status); + $this->assertEquals(0, $status); + // Modify our $PATH so that it begins with a path that contains an + // executable script named 'git' that always exits with 127, as if git were + // not found. Note that we run our tests using process isolation, so we do + // not need to restore the PATH when we are done. + $unavailableGitPath = $this->fixtures->binFixtureDir('disable-git-bin'); + chmod($unavailableGitPath . '/git', 0755); + putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH')); + // Confirm that 'git' is no longer available. + exec('git --help', $output, $status); + $this->assertEquals(127, $status); + // Run the scaffold command. + exec('composer composer:scaffold', $output, $status); + $this->assertFileExists($sut . '/docroot/index.php'); + $this->assertFileNotExists($sut . '/docroot/sites/default/.gitignore'); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php new file mode 100644 index 0000000000..8046662f16 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php @@ -0,0 +1,364 @@ +fileSystem = new Filesystem(); + $this->fixtures = new Fixtures(); + $this->projectRoot = $this->fixtures->projectRoot(); + // The directory used for creating composer projects to test can be + // configured using the SCAFFOLD_FIXTURE_DIR environment variable. Otherwise + // a directory will be created in the system's temporary directory. + $this->fixturesDir = getenv('SCAFFOLD_FIXTURE_DIR'); + if (!$this->fixturesDir) { + $this->fixturesDir = $this->fixtures->tmpDir($this->getName()); + } + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + // Remove any temporary directories et. al. that were created. + $this->fixtures->tearDown(); + } + + /** + * Creates the System-Under-Test. + * + * @param string $fixture_name + * The name of the fixture to use from + * core/tests/Drupal/Tests/Component/Scaffold/fixtures. + * @param array $replacements + * Key : value mappings for placeholders to replace in composer.json + * templates. + * + * @return string + * The path to the created System-Under-Test. + */ + protected function createSut($fixture_name, array $replacements = []) { + $sut = $this->fixturesDir . '/' . $fixture_name; + // Erase just our sut, to ensure it is clean. Recopy all of the fixtures. + $this->fileSystem->remove($sut); + $replacements += ['PROJECT_ROOT' => $this->projectRoot]; + $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements); + return $sut; + } + + /** + * Creates the system-under-test and runs a scaffold operation on it. + * + * @param string $fixture_name + * The name of the fixture to use from + * core/tests/Drupal/Tests/Component/Scaffold/fixtures. + * @param bool $is_link + * Whether to use symlinks for 'replace' operations. + * @param bool $relocated_docroot + * Whether the named fixture has a relocated document root. + */ + public function scaffoldSut($fixture_name, $is_link = FALSE, $relocated_docroot = TRUE) { + $sut = $this->createSut($fixture_name, ['SYMLINK' => $is_link ? 'true' : 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $sut); + // Test composer:scaffold. + $scaffoldOutput = $this->fixtures->runScaffold($sut); + + // Calculate the docroot directory and assert that our fixture layout + // matches what was stipulated in $relocated_docroot. Fail fast if + // the caller provided the wrong value. + $docroot = $sut; + if ($relocated_docroot) { + $docroot .= '/docroot'; + $this->assertFileExists($docroot); + } + else { + $this->assertFileNotExists($sut . '/docroot'); + } + + return [$docroot, $scaffoldOutput]; + } + + /** + * Data provider for testScaffoldWithExpectedException. + */ + public function scaffoldExpectedExceptionTestValues() { + return [ + [ + 'drupal-drupal-missing-scaffold-file', + 'Scaffold file assets/missing-robots-default.txt not found in package fixtures/drupal-drupal-missing-scaffold-file.', + TRUE, + ], + + [ + 'project-with-empty-scaffold-path', + 'No scaffold file path given for [web-root]/my-error in package fixtures/project-with-empty-scaffold-path', + FALSE, + ], + + [ + 'project-with-illegal-dir-scaffold', + 'Scaffold file assets in package fixtures/project-with-illegal-dir-scaffold is a directory; only files may be scaffolded', + FALSE, + ], + ]; + } + + /** + * Tests that scaffold files throw when they have bad values. + * + * @param string $fixture_name + * The name of the fixture to use from + * core/tests/Drupal/Tests/Component/Scaffold/fixtures. + * @param string $expected_exception_message + * The expected exception message. + * @param bool $is_link + * Whether or not symlinking should be used. + * + * @dataProvider scaffoldExpectedExceptionTestValues + */ + public function testScaffoldWithExpectedException($fixture_name, $expected_exception_message, $is_link) { + // Test scaffold. Expect an error. + $this->expectException(\Exception::class); + $this->expectExceptionMessage($expected_exception_message); + $this->scaffoldSut($fixture_name, $is_link); + } + + /** + * Try to scaffold a project that does not scaffold anything. + */ + public function testEmptyProject() { + $fixture_name = 'empty-fixture'; + $is_link = FALSE; + $relocated_docroot = FALSE; + + list($docroot, $scaffoldOutput) = $this->scaffoldSut($fixture_name, $is_link, $relocated_docroot); + $this->assertContains('Nothing scaffolded because no packages are allowed in the top-level composer.json file', $scaffoldOutput); + } + + /** + * Try to scaffold a project that allows a project with no scaffold files. + */ + public function testProjectThatScaffoldsEmptyProject() { + $fixture_name = 'project-allowing-empty-fixture'; + $is_link = FALSE; + $relocated_docroot = FALSE; + list($docroot, $scaffoldOutput) = $this->scaffoldSut($fixture_name, $is_link, $relocated_docroot); + $this->assertContains('The allowed package fixtures/empty-fixture does not provide a file mapping for Composer Scaffold', $scaffoldOutput); + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link); + } + + public function scaffoldOverridingSettingsExcludingHtaccessValues() { + return [ + [ + 'drupal-composer-drupal-project', + TRUE, + TRUE, + ], + + [ + 'drupal-drupal', + FALSE, + FALSE, + ], + ]; + } + + /** + * Asserts that the drupal/assets scaffold files correct for drupal/project and drupal/drupal. + * + * @param string $fixture_name + * The name of the fixture to use from + * core/tests/Drupal/Tests/Component/Scaffold/fixtures. + * @param bool $is_link + * Whether to use symlinks for 'replace' operations. + * @param bool $relocated_docroot + * Whether the named fixture has a relocated document root. + * + * @dataProvider scaffoldOverridingSettingsExcludingHtaccessValues + */ + public function testScaffoldOverridingSettingsExcludingHtaccess($fixture_name, $is_link, $relocated_docroot) { + list($docroot, $scaffoldOutput) = $this->scaffoldSut($fixture_name, $is_link, $relocated_docroot); + + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link); + $this->assertDefaultSettingsFromScaffoldOverride($docroot, $is_link); + $this->assertHtaccessExcluded($docroot); + } + + /** + * Asserts that the appropriate file was replaced. + * + * Check the drupal/drupal-based project to confirm that the expected file was + * replaced, and that files that were not supposed to be replaced remain + * unchanged. + */ + public function testDrupalDrupalFileWasReplaced() { + $fixture_name = 'drupal-drupal-test-overwrite'; + $is_link = FALSE; + $relocated_docroot = FALSE; + list($docroot, $scaffoldOutput) = $this->scaffoldSut($fixture_name, $is_link, $relocated_docroot); + + $this->assertScaffoldedFile($docroot . '/replace-me.txt', $is_link, '#from assets that replaces file#'); + $this->assertScaffoldedFile($docroot . '/keep-me.txt', $is_link, '#File in drupal-drupal-test-overwrite that is not replaced#'); + $this->assertScaffoldedFile($docroot . '/make-me.txt', $is_link, '#from assets that replaces file#'); + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link); + $this->assertScaffoldedFile($docroot . '/robots.txt', $is_link, "#{$fixture_name}#"); + } + + /** + * Test values for testDrupalDrupalFileWasAppended. + */ + public function scaffoldAppendTestValues() { + return array_merge( + $this->scaffoldAppendTestValuesToPermute(FALSE), + $this->scaffoldAppendTestValuesToPermute(TRUE) + ); + } + + /** + * Test values to run both with $is_link FALSE and $is_link TRUE. + * + * @param bool $is_link + * Whether or not symlinking should be used. + */ + protected function scaffoldAppendTestValuesToPermute($is_link) { + return [ + [ + 'drupal-drupal-test-append', + $is_link, + '#in drupal-drupal-test-append composer.json fixture.*This content is prepended to the top of the existing robots.txt fixture.*Test version of robots.txt from drupal/core.*This content is appended to the bottom of the existing robots.txt fixture.*in drupal-drupal-test-append composer.json fixture#ms', + ], + + [ + 'drupal-drupal-append-to-append', + $is_link, + '#in drupal-drupal-append-to-append composer.json fixture.*This content is prepended to the top of the existing robots.txt fixture.*Test version of robots.txt from drupal/core.*This content is appended to the bottom of the existing robots.txt fixture.*in profile-with-append composer.json fixture.*This content is appended to the bottom of the existing robots.txt fixture.*in drupal-drupal-append-to-append composer.json fixture#ms', + ], + ]; + } + + /** + * Tests a fixture where the robots.txt file is prepended / appended to. + * + * @param string $fixture_name + * The name of the fixture to use from + * core/tests/Drupal/Tests/Component/Scaffold/fixtures. + * @param bool $is_link + * Whether or not symlinking should be used. + * @param string $robots_txt_contents + * Regular expression matching expectations for robots.txt. + * + * @dataProvider scaffoldAppendTestValues + */ + public function testDrupalDrupalFileWasAppended($fixture_name, $is_link, $robots_txt_contents) { + list($docroot, $scaffoldOutput) = $this->scaffoldSut($fixture_name, $is_link, FALSE); + + $this->assertScaffoldedFile($docroot . '/robots.txt', FALSE, $robots_txt_contents); + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link); + } + + /** + * Asserts that the default settings file was overridden by the test. + * + * @param string $docroot + * The path to the System-under-Test's docroot. + * @param bool $is_link + * Whether or not symlinking is used. + */ + protected function assertDefaultSettingsFromScaffoldOverride($docroot, $is_link) { + $this->assertScaffoldedFile($docroot . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#'); + } + + /** + * Asserts that the .htaccess file was excluded by the test. + * + * @param string $docroot + * The path to the System-under-Test's docroot. + */ + protected function assertHtaccessExcluded($docroot) { + // 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 the scaffold files from drupal/assets are placed as expected. + * + * This tests that all assets from drupal/assets were scaffolded, save + * for .htaccess, robots.txt and default.settings.php, which are scaffolded + * in different ways in different tests. + * + * @param string $docroot + * The path to the System-under-Test's docroot. + * @param bool $is_link + * Whether or not symlinking is used. + */ + protected function assertCommonDrupalAssetsWereScaffolded($docroot, $is_link) { + $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/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 . '/update.php', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/web.config', $is_link, $from_core); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php new file mode 100644 index 0000000000..e381cd59ce --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php @@ -0,0 +1,58 @@ +destinationPath('[web-root]/robots.txt'); + $options = ScaffoldOptions::create([]); + // Assert that there is no target file before we run our test. + $this->assertFileNotExists($destination->fullPath()); + + // Create a file. + file_put_contents($destination->fullPath(), "# This is a test\n"); + + $prepend = $fixtures->sourcePath('drupal-drupal-test-append', 'prepend-to-robots.txt'); + $append = $fixtures->sourcePath('drupal-drupal-test-append', 'append-to-robots.txt'); + $sut = new AppendOp($prepend, $append); + + // Test the system under test. + $sut->process($destination, $fixtures->io(), $options); + // Assert that the target file was created. + $this->assertFileExists($destination->fullPath()); + // Assert the target contained the contents from the correct scaffold files. + $contents = trim(file_get_contents($destination->fullPath())); + $expected = <<assertEquals(trim($expected), $contents); + // Confirm that expected output was written to our io fixture. + $output = $fixtures->getOutput(); + $this->assertContains('Prepend to [web-root]/robots.txt from assets/prepend-to-robots.txt', $output); + $this->assertContains('Append to [web-root]/robots.txt from assets/append-to-robots.txt', $output); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php new file mode 100644 index 0000000000..2a690a0e6f --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php @@ -0,0 +1,130 @@ +getLocationReplacements(); + $file_mappings = [ + 'fixtures/drupal-assets-fixture' => [ + '[web-root]/index.php' => $fixtures->replaceOp('drupal-assets-fixture', 'index.php'), + '[web-root]/.htaccess' => $fixtures->replaceOp('drupal-assets-fixture', '.htaccess'), + '[web-root]/robots.txt' => $fixtures->replaceOp('drupal-assets-fixture', 'robots.txt'), + '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-assets-fixture', 'default.services.yml'), + ], + 'fixtures/drupal-profile' => [ + '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-profile', 'profile.default.services.yml'), + ], + 'fixtures/drupal-drupal' => [ + '[web-root]/.htaccess' => new SkipOp(), + '[web-root]/robots.txt' => $fixtures->appendOp('drupal-drupal-test-append', 'append-to-robots.txt'), + ], + ]; + $sut = new OperationCollection($fixtures->io()); + // Test the system under test. + list($scaffold_list, $resolved_file_mappings) = $this->callProtected($sut, 'collateScaffoldFiles', [$file_mappings, $locationReplacements]); + // Confirm that the keys of the output are the same as the keys of the + // input. + $this->assertEquals(array_keys($file_mappings), array_keys($resolved_file_mappings)); + // Also assert that we have the right ScaffoldFileInfo objects in the + // destination. + $this->assertResolvedToSameOp('fixtures/drupal-assets-fixture', '[web-root]/index.php', $file_mappings, $scaffold_list, $resolved_file_mappings); + $this->assertResolvedToSameOp('fixtures/drupal-profile', '[web-root]/sites/default/default.services.yml', $file_mappings, $scaffold_list, $resolved_file_mappings); + $this->assertResolvedToSameOp('fixtures/drupal-drupal', '[web-root]/robots.txt', $file_mappings, $scaffold_list, $resolved_file_mappings); + // Assert that the files below have been overridden. + $this->assertOverridden('fixtures/drupal-assets-fixture', '[web-root]/.htaccess', $scaffold_list, $resolved_file_mappings); + $this->assertOverridden('fixtures/drupal-assets-fixture', '[web-root]/robots.txt', $scaffold_list, $resolved_file_mappings); + } + + /** + * Checks to see if a given file was not overridden. + * + * The package name in the scaffold list for the provided destination should + * match the package name from the specified project. + * + * @param string $project + * The project to check. + * @param string $dest + * The destination to check. + * @param array $file_mappings + * The test file mappings keyed by project and destination. + * @param \Drupal\Component\Scaffold\ScaffoldFileInfo[] $scaffold_list + * The list of scaffolded files keyed by destination. + * @param array $resolved_file_mappings + * The list of resolved file mappings keyed by project and destination. + */ + protected function assertResolvedToSameOp($project, $dest, array $file_mappings, array $scaffold_list, array $resolved_file_mappings) { + $resolved_file_info = $resolved_file_mappings[$project][$dest]; + $this->assertEquals(get_class($resolved_file_info), ScaffoldFileInfo::class); + $resolved_scaffold_op = $resolved_file_info->op(); + // If this is an append op then it will be part of a conjunction op. + $expected = get_class($file_mappings[$project][$dest]); + if ($expected == AppendOp::class) { + $this->assertEquals(ConjunctionOp::class, get_class($resolved_scaffold_op)); + } + else { + $this->assertEquals($expected, get_class($resolved_scaffold_op)); + $this->assertEquals($file_mappings[$project][$dest], $resolved_scaffold_op); + } + $this->assertEquals($project, $scaffold_list[$dest]->packageName()); + } + + /** + * Checks if a given file was overridden. + * + * Assert that the file in the scaffold list at the specified destination + * comes from a different package than the one in the file info. + * + * @param string $project + * The project to check. + * @param string $dest + * The destination to check. + * @param \Drupal\Component\Scaffold\ScaffoldFileInfo[] $scaffold_list + * The list of scaffolded files keyed by destination. + * @param array $resolved_file_mappings + * The list of resolved file mappings keyed by project and destination. + */ + protected function assertOverridden($project, $dest, array $scaffold_list, array $resolved_file_mappings) { + $resolved_file_info = $resolved_file_mappings[$project][$dest]; + $this->assertEquals(get_class($resolved_file_info), ScaffoldFileInfo::class); + $this->assertNotEquals($project, $scaffold_list[$dest]->packageName()); + } + + /** + * Uses reflection to call a protected method of an object. + * + * @param mixed $obj + * The object to inspect. + * @param string $methodName + * The name of the method to call. + * @param array $args + * The arguments to pass to the protected method. + * + * @return mixed + * The return value from the protected method. + */ + protected function callProtected($obj, $methodName, array $args = []) { + $r = new \ReflectionMethod($obj, $methodName); + $r->setAccessible(TRUE); + return $r->invokeArgs($obj, $args); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php new file mode 100644 index 0000000000..a45d25635e --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php @@ -0,0 +1,40 @@ +destinationPath('[web-root]/robots.txt'); + $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt'); + $options = ScaffoldOptions::create([]); + $sut = new ReplaceOp($source, TRUE); + // Assert that there is no target file before we run our test. + $this->assertFileNotExists($destination->fullPath()); + // Test the system under test. + $sut->process($destination, $fixtures->io(), $options); + // Assert that the target file was created. + $this->assertFileExists($destination->fullPath()); + // Assert the target contained the contents from the correct scaffold file. + $contents = trim(file_get_contents($destination->fullPath())); + $this->assertEquals('# Test version of robots.txt from drupal/core.', $contents); + // Confirm that expected output was written to our io fixture. + $output = $fixtures->getOutput(); + $this->assertContains('Copy [web-root]/robots.txt from assets/robots.txt', $output); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php new file mode 100644 index 0000000000..27e358e6bf --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php @@ -0,0 +1,36 @@ +destinationPath('[web-root]/robots.txt'); + $options = ScaffoldOptions::create([]); + $sut = new SkipOp(); + // Assert that there is no target file before we run our test. + $this->assertFileNotExists($destination->fullPath()); + // Test the system under test. + $sut->process($destination, $fixtures->io(), $options); + // Assert that the target file was not created. + $this->assertFileNotExists($destination->fullPath()); + // Confirm that expected output was written to our io fixture. + $output = $fixtures->getOutput(); + $this->assertContains('Skip [web-root]/robots.txt: disabled', $output); + } + +} 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..0c8ab5bf80 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md @@ -0,0 +1,38 @@ +# Fixtures README + +These fixtures are automatically copied to a temporary directory during test +runs. After the test run, the fixtures are automatically deleted. + +Set the SCAFFOLD_FIXTURE_DIR environment variable to place the fixtures in a +specific location rather than a temporary directory. If this is done, then the +fixtures will not be deleted after the test run. This is useful for ad-hoc +testing. + +Example: + +$ SCAFFOLD_FIXTURE_DIR=$HOME/tmp/scaffold-fixtures composer unit +$ cd $HOME/tmp/scaffold-fixtures +$ cd drupal-drupal +$ composer composer:scaffold + +Scaffolding files for fixtures/drupal-assets-fixture: + - Link [web-root]/.csslintrc from assets/.csslintrc + - Link [web-root]/.editorconfig from assets/.editorconfig + - Link [web-root]/.eslintignore from assets/.eslintignore + - Link [web-root]/.eslintrc.json from assets/.eslintrc.json + - Link [web-root]/.gitattributes from assets/.gitattributes + - Link [web-root]/.ht.router.php from assets/.ht.router.php + - Skip [web-root]/.htaccess: overridden in my/project + - Link [web-root]/sites/default/default.services.yml from assets/default.services.yml + - Skip [web-root]/sites/default/default.settings.php: overridden in fixtures/scaffold-override-fixture + - Link [web-root]/sites/example.settings.local.php from assets/example.settings.local.php + - Link [web-root]/sites/example.sites.php from assets/example.sites.php + - Link [web-root]/index.php from assets/index.php + - Skip [web-root]/robots.txt: overridden in my/project + - Link [web-root]/update.php from assets/update.php + - Link [web-root]/web.config from assets/web.config +Scaffolding files for fixtures/scaffold-override-fixture: + - Link [web-root]/sites/default/default.settings.php from assets/override-settings.php +Scaffolding files for my/project: + - Skip [web-root]/.htaccess: disabled + - Link [web-root]/robots.txt from assets/robots-default.txt diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/assets/robots-default.txt new file mode 100644 index 0000000000..a26bf8912f --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/assets/robots-default.txt @@ -0,0 +1 @@ +# robots.txt fixture scaffolded from "file-mappings" in composer-hooks-fixture composer.json fixture. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl new file mode 100644 index 0000000000..f08fa60383 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl @@ -0,0 +1,67 @@ +{ + "name": "fixtures/drupal-drupal", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-scaffold": "*", + "fixtures/drupal-core-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/composer-hooks-nothing-allowed-fixture/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/assets/robots-default.txt new file mode 100644 index 0000000000..a26bf8912f --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/assets/robots-default.txt @@ -0,0 +1 @@ +# robots.txt fixture scaffolded from "file-mappings" in composer-hooks-fixture composer.json fixture. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/composer.json.tmpl new file mode 100644 index 0000000000..2acbea22f4 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/composer.json.tmpl @@ -0,0 +1,63 @@ +{ + "name": "fixtures/drupal-drupal", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-scaffold": "*", + "fixtures/drupal-core-fixture": "*" + }, + "extra": { + "composer-scaffold": { + "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/drupal-assets-fixture/assets/.csslintrc b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.csslintrc new file mode 100644 index 0000000000..f5bca65208 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.csslintrc @@ -0,0 +1 @@ +# Test version of .csslintrc from drupal/core. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.editorconfig b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.editorconfig new file mode 100644 index 0000000000..dcf0c98bbf --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.editorconfig @@ -0,0 +1 @@ +# Test version of .editorconfig from drupal/core. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintignore new file mode 100644 index 0000000000..f0405c03ad --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintignore @@ -0,0 +1 @@ +# Test version of .eslintignore from drupal/core. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintrc.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintrc.json new file mode 100644 index 0000000000..6391fb0c6e --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-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-assets-fixture/assets/.gitattributes b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.gitattributes new file mode 100644 index 0000000000..03436baebc --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.gitattributes @@ -0,0 +1 @@ +# Test version of .gitattributes from drupal/core. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.ht.router.php b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.ht.router.php new file mode 100644 index 0000000000..0cd34ad7ad --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.ht.router.php @@ -0,0 +1,2 @@ + + diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/composer.json new file mode 100644 index 0000000000..cb45ec5dc4 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/composer.json @@ -0,0 +1,24 @@ +{ + "name": "fixtures/drupal-assets-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-composer-drupal-project/.gitignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/.gitignore new file mode 100644 index 0000000000..19982ea3fd --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor \ No newline at end of file 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..207e3baa6c --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl @@ -0,0 +1,68 @@ +{ + "name": "fixtures/drupal-composer-drupal-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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-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-composer-drupal-project/docroot/.gitignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore new file mode 100644 index 0000000000..c795b054e5 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/README.md new file mode 100644 index 0000000000..7e59600739 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/README.md @@ -0,0 +1 @@ +# README diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/sites/default/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/sites/default/README.md new file mode 100644 index 0000000000..7e59600739 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/sites/default/README.md @@ -0,0 +1 @@ +# README 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..9aa29f55bf --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json @@ -0,0 +1,13 @@ +{ + "name": "fixtures/drupal-core-fixture", + "require": { + "fixtures/drupal-assets-fixture": "*" + }, + "extra": { + "composer-scaffold": { + "allowed-packages": [ + "fixtures/drupal-assets-fixture" + ] + } + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/assets/append-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/assets/append-to-robots.txt new file mode 100644 index 0000000000..5290d32590 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/assets/append-to-robots.txt @@ -0,0 +1,3 @@ +# :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +# This content is appended to the bottom of the existing robots.txt fixture. +# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-append-to-append composer.json fixture. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/assets/prepend-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/assets/prepend-to-robots.txt new file mode 100644 index 0000000000..8f7550ff5e --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/assets/prepend-to-robots.txt @@ -0,0 +1,3 @@ +# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-append-to-append composer.json fixture. +# This content is prepended to the top of the existing robots.txt fixture. +# :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/composer.json.tmpl new file mode 100644 index 0000000000..1919d878c6 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-append-to-append/composer.json.tmpl @@ -0,0 +1,71 @@ +{ + "name": "fixtures/drupal-drupal-test-append", + "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 + } + }, + "profile-with-append": { + "type": "path", + "url": "../profile-with-append", + "options": { + "symlink": true + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-scaffold": "*", + "fixtures/profile-with-append": "*", + "fixtures/drupal-core-fixture": "*" + }, + "extra": { + "composer-scaffold": { + "allowed-packages": [ + "fixtures/drupal-core-fixture", + "fixtures/profile-with-append" + ], + "locations": { + "web-root": "./" + }, + "symlink": __SYMLINK__, + "file-mapping": { + "[web-root]/.htaccess": false, + "[web-root]/robots.txt": { + "prepend": "assets/prepend-to-robots.txt", + "append": "assets/append-to-robots.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-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..8d9cf57293 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl @@ -0,0 +1,68 @@ +{ + "name": "fixtures/drupal-drupal-missing-scaffold-file", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-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-test-append/assets/append-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/append-to-robots.txt new file mode 100644 index 0000000000..8f05fc3b3a --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/append-to-robots.txt @@ -0,0 +1,3 @@ +# :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +# This content is appended to the bottom of the existing robots.txt fixture. +# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/prepend-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/prepend-to-robots.txt new file mode 100644 index 0000000000..995c204a6e --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/prepend-to-robots.txt @@ -0,0 +1,3 @@ +# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture. +# This content is prepended to the top of the existing robots.txt fixture. +# :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/composer.json.tmpl new file mode 100644 index 0000000000..9e6c579692 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/composer.json.tmpl @@ -0,0 +1,62 @@ +{ + "name": "fixtures/drupal-drupal-test-append", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-scaffold": "*", + "fixtures/drupal-core-fixture": "*" + }, + "extra": { + "composer-scaffold": { + "allowed-packages": [ + "fixtures/drupal-core-fixture" + ], + "locations": { + "web-root": "./" + }, + "symlink": __SYMLINK__, + "file-mapping": { + "[web-root]/.htaccess": false, + "[web-root]/robots.txt": { + "prepend": "assets/prepend-to-robots.txt", + "append": "assets/append-to-robots.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-test-overwrite/assets/replacement.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/replacement.txt new file mode 100644 index 0000000000..4e23d0c860 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/replacement.txt @@ -0,0 +1 @@ +# File from assets that replaces file in web root. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/robots-default.txt new file mode 100644 index 0000000000..28c7646d81 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/robots-default.txt @@ -0,0 +1 @@ +# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-overwrite composer.json fixture. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/composer.json.tmpl new file mode 100644 index 0000000000..d16205c612 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/composer.json.tmpl @@ -0,0 +1,80 @@ +{ + "name": "fixtures/drupal-drupal-test-overwrite", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-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", + "make-me.txt": { + "path": "assets/replacement.txt", + "overwrite": false + }, + "keep-me.txt": { + "path": "assets/replacement.txt", + "overwrite": false + }, + "replace-me.txt": { + "path": "assets/replacement.txt", + "overwrite": true + } + } + }, + "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-test-overwrite/keep-me.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/keep-me.txt new file mode 100644 index 0000000000..772a59531a --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/keep-me.txt @@ -0,0 +1 @@ +# File in drupal-drupal-test-overwrite that is not replaced by a scaffold file. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/replace-me.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/replace-me.txt new file mode 100644 index 0000000000..4147b02214 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/replace-me.txt @@ -0,0 +1 @@ +# File in drupal-drupal-test-overwrite that is replaced by a scaffold file. 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..a8641ef7a2 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl @@ -0,0 +1,74 @@ +{ + "name": "fixtures/drupal-drupal", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-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": "./" + }, + "gitignore": false, + "overwrite": true, + "symlink": __SYMLINK__, + "file-mapping": { + "[web-root]/.htaccess": false, + "[web-root]/robots.txt": { + "mode": "replace", + "path": "assets/robots-default.txt", + "overwrite": true + } + } + }, + "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-profile/assets/profile.default.services.yml b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/assets/profile.default.services.yml new file mode 100644 index 0000000000..2a35c02dba --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/assets/profile.default.services.yml @@ -0,0 +1,4 @@ +# default.services.yml fixture scaffolded from "file-mappings" in drupal-project composer.json fixture. +# Add a dummy key until YamlPecl can validate an empty YAML file: +# https://www.drupal.org/project/drupal/issues/3003300 +foo: bar diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/composer.json.tmpl new file mode 100644 index 0000000000..b3e4b76640 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/composer.json.tmpl @@ -0,0 +1,10 @@ +{ + "name": "fixtures/drupal-profile", + "extra": { + "composer-scaffold": { + "file-mapping": { + "[web-root]/.htaccess": false + } + } + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture-allowing-core/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture-allowing-core/composer.json new file mode 100644 index 0000000000..66e96bf38b --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture-allowing-core/composer.json @@ -0,0 +1,13 @@ +{ + "name": "fixtures/empty-fixture-allowing-core", + "extra": { + "composer-scaffold": { + "allowed-packages": [ + "fixtures/drupal-core-fixture" + ], + "locations": { + "web-root": "./" + } + } + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture/composer.json new file mode 100644 index 0000000000..7eb36a74cb --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture/composer.json @@ -0,0 +1,3 @@ +{ + "name": "fixtures/empty-fixture" +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/packages.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/packages.json new file mode 100644 index 0000000000..de4226972a --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/packages.json @@ -0,0 +1,14 @@ +{ + "packages": { + "fixtures/drupal-drupal": { + "dev-master": { + "name": "fixtures/drupal-drupal", + "version": "1.0.0", + "dist": { + "url": "./drupal-drupal", + "type": "path" + } + } + } + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/profile-with-append/assets/append-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/profile-with-append/assets/append-to-robots.txt new file mode 100644 index 0000000000..ab2eb43a74 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/profile-with-append/assets/append-to-robots.txt @@ -0,0 +1,3 @@ +# :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +# This content is appended to the bottom of the existing robots.txt fixture. +# robots.txt fixture scaffolded from "file-mappings" in profile-with-append composer.json fixture. diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/profile-with-append/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/profile-with-append/composer.json.tmpl new file mode 100644 index 0000000000..d67135fcac --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/profile-with-append/composer.json.tmpl @@ -0,0 +1,12 @@ +{ + "name": "fixtures/profile-with-append", + "extra": { + "composer-scaffold": { + "file-mapping": { + "[web-root]/robots.txt": { + "append": "assets/append-to-robots.txt" + } + } + } + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-allowing-empty-fixture/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-allowing-empty-fixture/composer.json.tmpl new file mode 100644 index 0000000000..254492a6e9 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-allowing-empty-fixture/composer.json.tmpl @@ -0,0 +1,77 @@ +{ + "name": "fixtures/project-allowing-empty-fixture", + "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 + } + }, + "drupal-assets-fixture": { + "type": "path", + "url": "../drupal-assets-fixture", + "options": { + "symlink": true + } + }, + "empty-fixture": { + "type": "path", + "url": "../empty-fixture", + "options": { + "symlink": true + } + }, + "scaffold-override-fixture": { + "type": "path", + "url": "../scaffold-override-fixture", + "options": { + "symlink": true + } + } + }, + "require": { + "drupal/core-composer-scaffold": "*", + "fixtures/drupal-core-fixture": "*", + "fixtures/empty-fixture": "*", + "fixtures/scaffold-override-fixture": "*" + }, + "extra": { + "composer-scaffold": { + "allowed-packages": [ + "fixtures/drupal-core-fixture", + "fixtures/empty-fixture", + "fixtures/scaffold-override-fixture" + ], + "locations": { + "web-root": "./" + }, + "gitignore": false, + "symlink": __SYMLINK__, + "file-mapping": { + "[web-root]/.htaccess": false + } + }, + "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/project-with-empty-scaffold-path/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-empty-scaffold-path/composer.json new file mode 100644 index 0000000000..e74df96ff0 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-empty-scaffold-path/composer.json @@ -0,0 +1,15 @@ +{ + "name": "fixtures/project-with-empty-scaffold-path", + "extra": { + "composer-scaffold": { + "locations": { + "web-root": "./" + }, + "file-mapping": { + "[web-root]/my-error": { + "path": "" + } + } + } + } +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/assets/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/assets/README.md new file mode 100644 index 0000000000..7e59600739 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/assets/README.md @@ -0,0 +1 @@ +# README diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/composer.json new file mode 100644 index 0000000000..5cebfd4b57 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/composer.json @@ -0,0 +1,15 @@ +{ + "name": "fixtures/project-with-illegal-dir-scaffold", + "extra": { + "composer-scaffold": { + "locations": { + "web-root": "./" + }, + "file-mapping": { + "[web-root]/assets": { + "path": "assets" + } + } + } + } +} 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..064ed7e3f5 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php @@ -0,0 +1,6 @@ +