commit 8dba9b1885ff9f46b558e8bf57ad4f6927c4d775 Author: Greg Anderson Date: Mon May 20 15:26:52 2019 -0700 Scaffold in core diff --git a/core/composer.json b/core/composer.json index 810ec27e59..75af602e33 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..97846e4dc4 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/AllowedPackages.php @@ -0,0 +1,160 @@ +composer = $composer; + $this->io = $io; + $this->manageOptions = $manageOptions; + $this->newPackages = []; + } + + /** + * Called when a newly-added package is discovered to contian scaffolding instructions. + */ + public function addedPackageWithScaffolding(PackageInterface $package) { + $this->newPackages[$package->getName()] = $package; + } + + /** + * 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(); + $jobType = $operation->getJobType(); + $reason = $operation->getReason(); + // Get the package. + $package = $operation->getJobType() == 'update' ? $operation->getTargetPackage() : $operation->getPackage(); + if (ScaffoldOptions::hasOptions($package->getExtra())) { + $this->addedPackageWithScaffolding($package); + } + } + + /** + * Recursivly build 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; + } + + /** + * Evaluate 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; + } + + /** + * Retrieve 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. +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..a0eb47850e --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php @@ -0,0 +1,100 @@ +vendorPath = $vendorPath; + } + + /** + * Generate 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 self + * Object wrapping the relative and absolute path to the destination file. + */ + public 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); + } + + /** + * Generate the autoload file at the specified location. + * + * This only writes a bit of PHP that includes the autoload file that + * Composer generated. Drupal does this so that it can guarentee that there + * will always be an `autoload.php` file in a well-known location. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $autoloadPath + * Where to write the autoload file. + * + * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult + * The result of the autoload file generation + */ + public function generateAutoload($package_name, $web_root) { + $autoloadPath = $this->autoloadPath($package_name, $web_root); + $location = dirname($autoloadPath->fullPath()); + // Calculate the relative path from the webroot (location of the project + // autoload.php) to the vendor directory. + $fs = new SymfonyFilesystem(); + $relativeVendorPath = $fs->makePathRelative($this->vendorPath, realpath($location)); + $fs->dumpFile($autoloadPath->fullPath(), $this->autoLoadContents($relativeVendorPath)); + return (new ScaffoldResult($autoloadPath))->setManaged(); + } + + /** + * Build the contents of the autoload file. + * + * @return string + * Return the contents for the autoload.php. + */ + protected function autoLoadContents($relativeVendorPath) { + $relativeVendorPath = rtrim($relativeVendorPath, '/'); + return <<composer = $composer; + $this->io = $io; + $this->manageOptions = new ManageOptions($composer); + $this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions); + $this->postPackageListeners = []; + } + + /** + * Post install command event to execute the scaffolding. + * + * @param \Composer\Script\Event $event + * The Composer event. + */ + public function onPostCmdEvent(Event $event) { + $this->scaffold(); + } + + /** + * The beforeRequire method is called before any 'require' event runs. + * + * @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; + } + + /** + * Post 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); + } + } + + /** + * 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. + */ + public 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 []; + } + } + + /** + * Create 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 (destination path => operation metadata array) + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface[] + * A list of scaffolding operation objects + */ + protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) { + $options = $this->manageOptions->getOptions(); + $scaffoldOpFactory = new OperationFactory($this->composer); + $scaffoldOps = []; + foreach ($package_file_mappings as $key => $value) { + $metadata = $this->normalizeScaffoldMetadata($key, $value); + $scaffoldOps[$key] = $scaffoldOpFactory->create($package, $key, $metadata, $options); + } + return $scaffoldOps; + } + + /** + * Normalize metadata, converting literal values into arrays with the same meaning. + * + * Conversions performed include: + * - Boolean 'false' means "skip". + * - A string menas "replace", with the string value becoming the path. + * + * @param string $key + * The key (destination path) for the value to normalize. + * @param mixed $value + * The metadata for this operation object, which varies by operation type. + * + * @return array + * Normalized scaffold metadata. + */ + protected function normalizeScaffoldMetadata($key, $value) { + if (is_bool($value)) { + if (!$value) { + return ['mode' => 'skip']; + } + throw new \Exception("File mapping {$key} cannot be given the value 'true'."); + } + if (empty($value)) { + throw new \Exception("File mapping {$key} 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; + } + + /** + * 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)) { + 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(); + $scaffoldCollection->coalateScaffoldFiles($file_mappings, $locationReplacements); + // Write the collected scaffold files to the designated location on disk. + $scaffoldResults = $scaffoldCollection->processScaffoldFiles($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. + $generator = new GenerateAutoloadReferenceFile($this->getVendorPath()); + $scaffoldResults[] = $generator->generateAutoload($this->rootPackageName(), $this->getWebRoot()); + // 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); + } + + /** + * Retrieve the path to the web root. + * + * Note that only the root package can define the web root. + * + * @return string + * The file path of the web root. + * + * @throws \Exception + */ + public function getWebRoot() { + return $this->manageOptions->getOptions()->requiredLocation('web-root', "The extra.composer-scaffold.location.web-root is not set in composer.json."); + } + + /** + * Get the path to the 'vendor' directory. + * + * @return string + * The file path of the vendor directory. + */ + public function getVendorPath() { + $vendorDir = $this->composer->getConfig()->get('vendor-dir'); + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($vendorDir); + 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; + } + + /** + * Get 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..188d228b69 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Interpolator.php @@ -0,0 +1,141 @@ +startToken = $startToken; + $this->endToken = $endToken; + $this->data = []; + } + + /** + * SetData allows the client to associate a standard data set to use when interpolating. + * + * @param array $data + * Interpolation data to use when interpolating. + */ + public function setData(array $data) { + $this->data = $data; + return $this; + } + + /** + * Add data allows the client to add to the standard data set to use when interpolating. + * + * @param array $data + * Interpolation data to use when interpolating. + */ + public function addData(array $data) { + $this->data = array_merge($this->data, $data); + return $this; + } + + /** + * Interpolate replaces tokens in a string with values from an associative array. + * + * Tokens are surrounded by double curley braces, e.g. "[key]". The characters + * that surround the key may be defined when the Interpolator is constructed. + * + * Example: + * If the message is 'Hello, [user.name]', then the value of the user.name + * item is fetched from the array, and the token [user.name] is + * replaced with the result. + * + * @param string $message + * Message containing tokens to be replaced. + * @param mixed|array $extra + * Data to use for interpolation in addition to whatever was provided by self::setData(). + * @param string|bool $default + * The value to substitute for tokens that + * are not found in the data. If `false`, then missing + * tokens are not replaced. + * + * @return string + * The message after replacements have been made. + */ + public function interpolate($message, $extra = [], $default = '') { + $data = $extra + $this->data; + $replacements = $this->replacements($message, $data, $default); + return strtr($message, $replacements); + } + + /** + * FindTokens finds all of the tokens in the provided message. + * + * @param string $message + * String with tokens. + * + * @return string[] + * map of token to key, e.g. {{key}} => key + */ + public function findTokens($message) { + $regEx = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#'; + if (!preg_match_all($regEx, $message, $matches, PREG_SET_ORDER)) { + return []; + } + $tokens = []; + foreach ($matches as $matchSet) { + list($sourceText, $key) = $matchSet; + $tokens[$sourceText] = $key; + } + return $tokens; + } + + /** + * Replacements finds the tokens that exist in a message and builds a replacement array. + * + * All of the replacements in the data array are looked up given the token + * keys from the provided message. Keys that do not exist in the configuration + * are replaced with the default value. + */ + protected function replacements($message, $data, $default = '') { + $tokens = $this->findTokens($message); + $replacements = []; + foreach ($tokens as $sourceText => $key) { + $replacementText = $this->get($key, $data, $default); + if ($replacementText !== FALSE) { + $replacements[$sourceText] = $replacementText; + } + } + return $replacements; + } + + /** + * Get a value from an array. + */ + protected function get($key, $data, $default) { + return array_key_exists($key, $data) ? $data[$key] : $default; + } + +} 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..1f913eaeb8 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php @@ -0,0 +1,175 @@ +dir = $dir; + } + + /** + * Determine 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). + */ + public function checkIgnore($path) { + $process = new Process('git check-ignore ' . $path, $this->dir); + $process->run(); + $isIgnored = $process->getExitCode() == 0; + return $isIgnored; + } + + /** + * Determine whether the specified scaffold file is tracked in the repository. + * + * @param string $path + * Path to scaffold file to check. + * + * @return bool + * Whether the specified file is already tracked or not (TRUE if tracked). + */ + public function checkTracked($path) { + $process = new Process('git ls-files --error-unmatch ' . $path, $this->dir); + $process->run(); + $isTracked = $process->getExitCode() == 0; + return $isTracked; + } + + /** + * Check to see if the project root dir is in a git repository. + * + * @return bool + * True if this is a repository. + */ + public function isRepository() { + $process = new Process('git rev-parse --show-toplevel', $this->dir); + $process->run(); + $isRepository = $process->getExitCode() == 0; + return $isRepository; + } + + /** + * Check to see if the vendor directory is git ignored. + * + * @return bool + * True if 'vendor' is committed, or false if it is ignored. + */ + public function vendorCommitted() { + return $this->checkTracked('vendor'); + } + + /** + * Determine whether we should manage gitignore files. + * + * @param ScaffoldOptions $options + * Configuration options from the composer.json extras section. + * + * @return bool + * Whether or not gitignore files should be managed. + */ + public 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(); + } + + /** + * Manage gitignore files. + * + * @param array $files + * A list of scaffold results, each of which holds a path and whether + * or not that file is managed. + * @param ScaffoldOptions $options + * Configuration options from the composer.json extras section. + */ + public function manageIgnored(array $files, ScaffoldOptions $options) { + if (!$this->managementOfGitIgnoreEnabled($options)) { + return; + } + // Accumulate entried 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()); + $isTracked = $this->checkTracked($scaffoldResult->destination()->fullPath()); + if (!$isIgnored && !$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); + } + } + + /** + * Add 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. + */ + public function addToGitIgnore($dir, array $entries) { + sort($entries); + $gitIgnorePath = $dir . '/.gitignore'; + $contents = $this->gitIgnoreContents($gitIgnorePath); + $contents .= implode("\n", $entries); + file_put_contents($gitIgnorePath, $contents); + } + + /** + * Fetch the current contents of the specified .gitignore file. + * + * @param string $gitIgnorePath + * Path to .gitignore file. + * + * @return string + * Contents of .gitignore. Will always end with a "\n" unless empty. + */ + public function gitIgnoreContents($gitIgnorePath) { + if (!file_exists($gitIgnorePath)) { + return ''; + } + $contents = file_get_contents($gitIgnorePath); + if (!empty($contents) && substr($contents, -1) != "\n") { + $contents .= "\n"; + } + return $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..6c430a09ed --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ManageOptions.php @@ -0,0 +1,85 @@ +composer = $composer; + } + + /** + * Get the root-level scaffold options for this project. + * + * @return ScaffoldOptions + * The scaffold otpions object + */ + public function getOptions() { + return $this->packageOptions($this->composer->getPackage()); + } + + /** + * The scaffold options for the stipulated project. + * + * @param \Composer\Package\PackageInterface $package + * The package to fetch the scaffold options from. + * + * @return ScaffoldOptions + * The scaffold otpions object + */ + public function packageOptions(PackageInterface $package) { + return ScaffoldOptions::create($package->getExtra()); + } + + /** + * GetLocationReplacements 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 Interpolator + * Object that will do replacements in a string using tokens in 'locations' element. + */ + public function getLocationReplacements() { + return (new Interpolator())->setData($this->ensureLocations()); + } + + /** + * Ensure that all of the locatons defined in the scaffold filed exist. + * + * Create them on the filesystem if they do not. + */ + public 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..d50b1b4218 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php @@ -0,0 +1,132 @@ +prepend = $prependPath; + return $this; + } + + /** + * Set the relative path to the append file. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $appendPath + * The relative path to the append file file. + * + * @return $this + */ + public function setAppendFile(ScaffoldFilePath $appendPath) { + $this->append = $appendPath; + return $this; + } + + /** + * Add interpolation data for our append and prepend source files. + * + * @param \Drupal\Component\Scaffold\Interpolator $interpolator + * Interpolator to add data to. + */ + protected function addInterpolationData(Interpolator $interpolator) { + if (isset($this->prepend)) { + $this->prepend->addInterpolationData($interpolator, 'prepend'); + } + if (isset($this->append)) { + $this->append->addInterpolationData($interpolator, 'append'); + } + } + + /** + * Append or prepend information onto the overridden scaffold file. + * + * {@inheritdoc} + */ + public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { + $interpolator = $destination->getInterpolator(); + $this->addInterpolationData($interpolator); + $destination_path = $destination->fullPath(); + // It is not possible to append / prepend unless the destination path + // is the same as some scaffold file provided by an earlier package. + if (!$this->hasOriginalOp()) { + throw new \Exception($interpolator->interpolate("Cannot append/prepend because no prior package provided a scaffold file at that [dest-rel-path].")); + } + // 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->originalOp()->process($destination, $io, $options->overrideSymlink(FALSE)); + // Fetch the prepend contents, if provided. + $prependContents = ''; + if (!empty($this->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)) { + $appendContents = "\n" . file_get_contents($this->append->fullPath()); + $io->write($interpolator->interpolate(" - Append to [dest-rel-path] from [append-rel-path]")); + } + $this->append($destination, $prependContents, $appendContents); + return (new ScaffoldResult($destination))->setManaged(); + } + + /** + * Do the actuall append / prepend operation for the provided scaffold file. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination + * The scaffold file to append / prepend to. + * @param string $prependContents + * The contents to add to the beginning of the file. + * @param string $appendContents + * The contents to add to the end of the file. + */ + protected function append(ScaffoldFilePath $destination, $prependContents, $appendContents) { + $interpolator = $destination->getInterpolator(); + $destination_path = $destination->fullPath(); + // Exit early if there is no append / prepend data. + if (empty(trim($prependContents)) && empty(trim($appendContents))) { + $io->write($interpolator->interpolate(" - Keep [dest-rel-path] unchanged: no content to prepend / append was provided.")); + return; + } + // We're going to assume that none of these files are going to be + // very large, so we will just load them all into memory for now. + // We'd want to use streaminig if we thought that anyone would scaffold + // and append very large files. + $originalContents = file_get_contents($destination_path); + // Write the appended / prepended contents back to the file. + $alteredContents = $prependContents . $originalContents . $appendContents; + file_put_contents($destination_path, $alteredContents); + } + +} 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..663ff30682 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php @@ -0,0 +1,154 @@ + operation mappings. + * + * @var array + */ + protected $listOfScaffoldFiles; + /** + * Associative array containing package name => file mappings. + * + * @var array + */ + protected $resolvedFileMappings; + /** + * Composer's I/O service. + * + * @var \Composer\IO\IOInterface + */ + protected $io; + + /** + * OperationCollection constructor. + * + * @param \Composer\IO\IOInterface $io + * A reference to the IO object, to allow us to write progress messages + * as we process scaffold operations. + */ + public function __construct(IOInterface $io) { + $this->io = $io; + } + + /** + * Fetch the file mappings. + * + * @return array + * Associative array containing package name => file mappings + */ + public function fileMappings() { + return $this->resolvedFileMappings; + } + + /** + * Get the final list of what should be scaffolded where. + * + * @return array + * Associative array containing destination => operation mappings + */ + public function scaffoldList() { + return $this->listOfScaffoldFiles; + } + + /** + * Return the package name that provides the scaffold file info at this destination path. + * + * Given the list of all scaffold file info objects, return the package that + * provides the scaffold file info for the scaffold file that will be placed + * at the destination that this scaffold file would be placed at. Note that + * this will be the same as $scaffold_file->packageName() unless this scaffold + * file has been overridden or removed by some other package. + * + * @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. + */ + public function findProvidingPackage(ScaffoldFileInfo $scaffold_file) { + // The scaffold file should always be in our list, but we will check + // just to be sure that it really is. + $scaffoldList = $this->scaffoldList(); + $dest_rel_path = $scaffold_file->destination()->relativePath(); + if (!array_key_exists($dest_rel_path, $scaffoldList)) { + throw new \Exception("Scaffold file not found in list of all scaffold files."); + } + $overridden_scaffold_file = $scaffoldList[$dest_rel_path]; + return $overridden_scaffold_file->packageName(); + } + + /** + * Copy all files, as defined by $file_mappings. + * + * @param array $file_mappings + * An multidimensional array of file mappings, as returned by + * self::getFileMappingsFromPackages(). + * @param \Drupal\Component\Scaffold\Interpolator $locationReplacements + * An object with the location mappings (e.g. [web-root]). + */ + public function coalateScaffoldFiles(array $file_mappings, Interpolator $locationReplacements) { + $resolved_file_mappings = []; + $resolved_package_file_list = []; + $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, $locationReplacements); + $scaffold_file = (new ScaffoldFileInfo())->setDestination($destination)->setOp($op); + // If there was already a scaffolding operation happening at this + // path, then pass it along to the new scaffold op, if it cares. + if (isset($list_of_scaffold_files[$destination_rel_path]) && $op instanceof OriginalOpAwareInterface) { + $op->setOriginalOp($list_of_scaffold_files[$destination_rel_path]->op()); + } + $list_of_scaffold_files[$destination_rel_path] = $scaffold_file; + $resolved_file_mappings[$package_name][$destination_rel_path] = $scaffold_file; + } + } + $this->listOfScaffoldFiles = $list_of_scaffold_files; + $this->resolvedFileMappings = $resolved_file_mappings; + } + + /** + * Scaffolds the files in our scaffold collection, package-by-package. + * + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the top-level composer.json file. + * + * @return ScaffoldResult[] + * Associative array of destination path : scaffold result for each scaffolded file. + */ + public function processScaffoldFiles(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 ($this->fileMappings() 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($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..35afee5b7b --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php @@ -0,0 +1,148 @@ +composer = $composer; + } + + /** + * Create a scaffolding operation object of an appropriate for the provided metadata. + * + * @param \Composer\Package\PackageInterface $package + * The package that relative paths will be relative from. + * @param string $dest_rel_path + * The destination path for the scaffold file. Used only for error messages. + * @param mixed $value + * The metadata for this operation object, which varies by operation type. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the top-level composer.json file. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * The scaffolding operation object (skip, replace, etc.) + */ + public function create(PackageInterface $package, $dest_rel_path, $value, ScaffoldOptions $options) { + switch ($value['mode']) { + case 'skip': + return new SkipOp(); + + case 'replace': + return $this->createReplaceOp($package, $dest_rel_path, $value, $options); + + case 'append': + return $this->createAppendOp($package, $dest_rel_path, $value, $options); + } + throw new \Exception("Unknown scaffold opperation mode {$value['mode']}."); + } + + /** + * Create 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 $dest_rel_path + * 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'. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the top-level composer.json file. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * A scaffold replace operation obejct. + */ + protected function createReplaceOp(PackageInterface $package, $dest_rel_path, array $metadata, ScaffoldOptions $options) { + $op = new ReplaceOp(); + // If this op does not provide an 'overwrite' value, default it to true. + $metadata += ['overwrite' => TRUE]; + if (!isset($metadata['path'])) { + throw new \Exception("'path' component required for 'replace' operations."); + } + $package_name = $package->getName(); + $package_path = $this->getPackagePath($package); + $source = ScaffoldFilePath::sourcePath($package_name, $package_path, $dest_rel_path, $metadata['path']); + $op->setSource($source)->setOverwrite($metadata['overwrite']); + return $op; + } + + /** + * Create an 'append' (or 'prepend') scaffold op. + * + * @param \Composer\Package\PackageInterface $package + * The package that relative paths will be relative from. + * @param string $dest_rel_path + * 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'. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Configuration options from the top-level composer.json file. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * A scaffold replace operation obejct. + */ + protected function createAppendOp(PackageInterface $package, $dest_rel_path, array $metadata, ScaffoldOptions $options) { + $op = new AppendOp(); + $package_name = $package->getName(); + $package_path = $this->getPackagePath($package); + if (isset($metadata['prepend'])) { + $prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $dest_rel_path, $metadata['prepend']); + $op->setPrependFile($prepend_source_file); + } + if (isset($metadata['append'])) { + $append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $dest_rel_path, $metadata['append']); + $op->setAppendFile($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); + } + } + +} 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..a9526b0cb3 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php @@ -0,0 +1,29 @@ +originalOp = $originalOp; + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasOriginalOp() { + return isset($this->originalOp); + } + + /** + * {@inheritdoc} + */ + public function originalOp() { + return $this->originalOp; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Operations/ReplaceOp.php b/core/lib/Drupal/Component/Scaffold/Operations/ReplaceOp.php new file mode 100644 index 0000000000..344b08544b --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/ReplaceOp.php @@ -0,0 +1,143 @@ +source = $sourcePath; + return $this; + } + + /** + * Get the source. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * The source file reference object. + */ + public function getSource() { + return $this->source; + } + + /** + * Set whether the scaffold file should overwrite existing files at the same path. + * + * @param bool $overwrite + * Whether to overwrite existing files. + * + * @return $this + */ + public function setOverwrite($overwrite) { + $this->overwrite = $overwrite; + return $this; + } + + /** + * Determine whether scaffold file should overwrite files already at the same path. + * + * @return bool + * Value of the 'overwrite' option. + */ + public function getOverwrite() { + return $this->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->getOverwrite() === 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))->setManaged(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, $options); + } + return $this->copyScaffold($destination, $io, $options); + } + + /** + * Copy the scaffold file. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination + * Scaffold file to process. + * @param \Composer\IO\IOInterface $io + * IOInterface to writing to. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Various options that may alter the behavior of the operation. + */ + public function copyScaffold(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { + $interpolator = $destination->getInterpolator(); + $this->getSource()->addInterpolationData($interpolator); + $success = copy($this->getSource()->fullPath(), $destination->fullPath()); + if (!$success) { + throw new \Exception($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))->setManaged($this->getOverwrite()); + } + + /** + * Symlink the scaffold file. + * + * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination + * Scaffold file to process. + * @param \Composer\IO\IOInterface $io + * IOInterface to writing to. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $options + * Various options that may alter the behavior of the operation. + */ + public function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { + $interpolator = $destination->getInterpolator(); + $source_path = $this->getSource()->fullPath(); + $destination_path = $destination->fullPath(); + try { + $fs = new Filesystem(); + $fs->relativeSymlink($this->getSource()->fullPath(), $destination->fullPath()); + } + catch (\Exception $e) { + throw new \Exception($interpolator->interpolate("Could not symlink source file [src-rel-path] to [dest-rel-path]! "), 1, $e); + } + $io->write($interpolator->interpolate(" - Link [dest-rel-path] from [src-rel-path]")); + return (new ScaffoldResult($destination))->setManaged($this->getOverwrite()); + } + +} 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..c147a93be3 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php @@ -0,0 +1,68 @@ +destination = $destination; + $this->managed = FALSE; + } + + /** + * Determine whether this scaffold file is managed. + * + * @return bool + * Whether the scaffold file was managed by this plugin (scaffolded) or not (skipped). + */ + public function isManaged() { + return $this->managed; + } + + /** + * Recored whether this result was managed or unmanaged. + * + * @param bool $isManaged + * Whether this result is managed. + * + * @return $this + */ + public function setManaged($isManaged = TRUE) { + $this->managed = $isManaged; + return $this; + } + + /** + * 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..d61b26c3a2 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php @@ -0,0 +1,25 @@ +getInterpolator(); + $io->write($interpolator->interpolate(" - Skip [dest-rel-path]: disabled")); + return (new ScaffoldResult($destination))->setManaged(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..61a39b9c6d --- /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->onPostCmdEvent($event); + } + + /** + * Post package event behaviour. + * + * @param \Composer\Installer\PackageEvent $event + * Composer package event sent on install/update/remove. + */ + public function postPackage(PackageEvent $event) { + $this->handler->onPostPackageEvent($event); + } + + /** + * 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..c1ec09cc55 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php @@ -0,0 +1,20 @@ +op = $op; + return $this; + } + + /** + * Get the Scaffold operation. + * + * @return \Drupal\Component\Scaffold\Operations\OperationInterface + * Operations object that handles scaffolding (copy, make symlink, etc). + */ + public function op() { + return $this->op; + } + + /** + * Get the package name. + * + * @return string + * The name of the package this scaffold file info was collected from. + */ + public function packageName() { + return $this->destination->packageName(); + } + + /** + * Set the relative path to the destination. + * + * @param \Drupal\Component\Scaffold\Operations\ScaffoldFilePath $destination + * The full and relative paths to the destination file and the package defining it. + * + * @return $this + */ + public function setDestination(ScaffoldFilePath $destination) { + $this->destination = $destination; + return $this; + } + + /** + * Get the destination. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * The scaffold path to the destination file. + */ + public function destination() { + return $this->destination; + } + + /** + * Determine if this scaffold file has been overridden by another package. + * + * @param string $providing_package + * The name of the package that provides the scaffold file at this location, + * as returned by self::findProvidingPackage() + * + * @return bool + * Whether this scaffold file if overridden or removed. + */ + public function overridden($providing_package) { + return $this->packageName() !== $providing_package; + } + + /** + * Interpolate a string using the data from this scaffold file info. + * + * @return Interpolator + * An interpolator for making string replacements. + */ + public function getInterpolator() { + return $this->destination->getInterpolator(); + } + + /** + * Given a message with placeholders, return the interpolated result. + * + * @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 ScaffoldOptions $options + * Assorted operational options, e.g. whether the destination should be a symlink. + * + * @throws \Exception + */ + public function process(IOInterface $io, ScaffoldOptions $options) { + return $this->op()->process($this->destination, $io, $options); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php b/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php new file mode 100644 index 0000000000..b9a04df5af --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php @@ -0,0 +1,183 @@ +type = $path_type; + $this->packageName = $package_name; + $this->relPath = $rel_path; + $this->fullPath = $full_path; + } + + /** + * The name of the package this source file was pulled from. + * + * @return string + * Name of package. + */ + public function packageName() { + return $this->packageName; + } + + /** + * The relative path to the source file (best to use in messages). + * + * @return string + * Relative path to file. + */ + public function relativePath() { + return $this->relPath; + } + + /** + * The full path to the source file. + * + * @return string + * Full path to file. + */ + public function fullPath() { + return $this->fullPath; + } + + /** + * Convert 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 \Exception("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 \Exception("Scaffold file {$source} not found in package {$package_name}."); + } + if (is_dir($source_full_path)) { + throw new \Exception("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); + } + + /** + * Convert 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 $locationReplacements + * 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 $locationReplacements) { + $dest_full_path = $locationReplacements->interpolate($destination); + return new self('dest', $package_name, $destination, $dest_full_path); + } + + /** + * Add data about the relative and full path to this item to the provided interpolator. + * + * @param \Drupal\Component\Scaffold\Interpolator $interpolator + * Interpolator to add data to. + * @param string $namePrefix + * Prefix to add before -rel-path and -full-path item names. Defaults to path type. + */ + public function addInterpolationData(Interpolator $interpolator, $namePrefix = '') { + if (empty($namePrefix)) { + $namePrefix = $this->type; + } + $data = [ + 'package-name' => $this->packageName(), + "{$namePrefix}-rel-path" => $this->relativePath(), + "{$namePrefix}-full-path" => $this->fullPath(), + ]; + $interpolator->addData($data); + } + + /** + * Interpolate a string using the data from this scaffold file info. + * + * @param string $namePrefix + * Prefix to add before -rel-path and -full-path item names. Defaults to path type. + * + * @return \Drupal\Component\Scaffold\Interpolator + * An interpolator for making string replacements. + */ + public function getInterpolator($namePrefix = '') { + $interpolator = new Interpolator(); + $this->addInterpolationData($interpolator, $namePrefix); + 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..7357fdb0b9 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php @@ -0,0 +1,219 @@ +options = $options + [ + "allowed-packages" => [], + "locations" => [], + "symlink" => FALSE, + "file-mapping" => [], + ]; + } + + /** + * Determine 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); + } + + /** + * Create 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); + } + + /** + * Create a scaffold option object with default values. + * + * @return self + * A scaffold options object with default values + */ + public static function defaultOptions() { + return new self([]); + } + + /** + * Create 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); + } + + /** + * Create a new scaffold options object with a new value in the 'symlink' variable. + * + * @return self + * The scaffold options object representing the provided scaffold options + */ + public function overrideSymlink($symlink) { + return $this->override(['symlink' => $symlink]); + } + + /** + * Determine whether any allowed packages were defined. + * + * @return bool + * Whether there are allowed packages + */ + public function hasAllowedPackages() { + return !empty($this->allowedPackages()); + } + + /** + * The allowed packages from these options. + * + * @return array + * The list of allowed packages + */ + public function allowedPackages() { + return $this->options['allowed-packages']; + } + + /** + * The location mapping table, e.g. 'webroot' => './'. + * + * @return array + * A map of name : location values + */ + public function locations() { + return $this->options['locations']; + } + + /** + * Determine whether a given named location is defined. + * + * @return bool + * True if the specified named location exist. + */ + public function hasLocation($name) { + return array_key_exists($name, $this->locations()); + } + + /** + * Get 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; + } + + /** + * Return the value of a specific named location, or throw. + * + * @param string $name + * The name of the location to fetch. + * @param string $message + * The message to pass into the exception if the requested location + * does not exist. + * + * @return string + * The value of the provided named location + */ + public function requiredLocation($name, $message) { + if (!$this->hasLocation($name)) { + throw new \Exception($message); + } + return $this->getLocation($name); + } + + /** + * Determine whether the options have defined symlink mode. + * + * @return bool + * Whether or not 'symlink' mode + */ + public function symlink() { + return $this->options['symlink']; + } + + /** + * Determine whether these options contain file mappings. + * + * @return bool + * Whether or not the scaffold options contain any file mappings + */ + public function hasFileMapping() { + return !empty($this->fileMapping()); + } + + /** + * Return the actual file mappings. + * + * @return array + * File mappings for just this config type. + */ + public function fileMapping() { + return $this->options['file-mapping']; + } + + /** + * Whether the scaffold options have 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']); + } + + /** + * 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..5b59d5a909 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/composer.json @@ -0,0 +1,36 @@ +{ + "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\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "Drupal\\Tests\\Component\\Scaffold\\": "tests" + } + }, + "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", + "drupal/coder": "^8.3.2", + "phpunit/phpunit": "^4.8.35 || ^6.5" + } +} 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..39d0165c5b --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php @@ -0,0 +1,20 @@ +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..14c44ee6f4 --- /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 \Exception("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..802d9a3aeb --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php @@ -0,0 +1,329 @@ +io) { + $this->io = new BufferIO(); + } + return $this->io; + } + + /** + * Get 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; + } + + /** + * Get the output from our io() fixture. + * + * @return string + * Output captured from tests that write to Fixtures::io(). + */ + public function getOutput() { + return $this->io()->getOutput(); + } + + /** + * Return the path to this project so that it may be injected into composer.json files. + * + * @return string + * Path to the root of this project. + */ + public function projectRoot() { + return realpath(__DIR__) . '/../../../../../../core/lib/Drupal/Component/Scaffold'; + } + + /** + * Return the path to the project fixtures. + * + * @return string + * Path to project fixtures + */ + public function allFixturesDir() { + return realpath(__DIR__ . '/fixtures'); + } + + /** + * Return the path to one particular project fixture. + * + * @return string + * Path to project fixture + */ + public function projectFixtureDir($project_name) { + $dir = $this->allFixturesDir() . '/' . $project_name; + if (!is_dir($dir)) { + throw new \Exception("Requested fixture project {$project_name} that does not exist."); + } + return $dir; + } + + /** + * Return the path to one particular bin path. + * + * @return string + * Path to project fixture + */ + public function binFixtureDir($bin_name) { + $dir = $this->allFixturesDir() . '/scripts/' . $bin_name; + if (!is_dir($dir)) { + throw new \Exception("Requested fixture bin dir {$bin_name} that does not exist."); + } + return $dir; + } + + /** + * Use in place of ScaffoldFilePath::sourcePath to get a path to a source scaffold 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". + * @param string $destination + * The path to the destination; only used in error messages, not needed for most tests. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * The full and relative path to the desired asset + */ + public function sourcePath($project_name, $source, $destination = 'unknown') { + $package_name = "fixtures/{$project_name}"; + $source_rel_path = "assets/{$source}"; + $package_path = $this->projectFixtureDir($project_name); + $destination = 'unknown'; + return ScaffoldFilePath::sourcePath($package_name, $package_path, $destination, $source_rel_path); + } + + /** + * Use in place of Handler::getLocationReplacements() to obtain a 'web-root'. + * + * @return \Drupal\Component\Scaffold\Interpolator + * An interpolator with location replacements, including 'web-root'. + */ + public function getLocationReplacements() { + $destinationTmpDir = $this->mkTmpDir(); + $interpolator = new Interpolator(); + $interpolator->setData(['web-root' => $destinationTmpDir, 'package-name' => 'fixtures/tmp-destination']); + return $interpolator; + } + + /** + * Use to create 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())->setSource($source_path); + } + + /** + * Use to create 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 opperation object. + */ + public function appendOp($project_name, $source) { + $source_path = $this->sourcePath($project_name, $source); + return (new AppendOp())->setAppendFile($source_path); + } + + /** + * Use in place of ScaffoldFilePath::destinationPath to get a destination path in a tmp dir. + * + * @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 + * The name of the fixture package that this path came from. Optional; + * taken from interpolator if not provided. + * + * @return \Drupal\Component\Scaffold\ScaffoldFilePath + * A destination scaffold file backed by temporary storage. + */ + 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); + } + + /** + * Generate 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; + } + + /** + * Create 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; + } + + /** + * Call '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->fixturesDir = NULL; + $this->io = NULL; + } + + /** + * Create 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('__', '__', TRUE); + $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); + } + } + } + + /** + * Run 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. + */ + 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 array + * 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); + } + catch (\Exception $e) { + print "Exception: " . $e->getMessage() . "\n"; + } + if ($exitCode != $expectedExitCode) { + print "Command '{$cmd}' - Expected exit code: {$expectedExitCode}, actual exit code: {$exitCode}\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..12fd9f1166 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php @@ -0,0 +1,138 @@ +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, $stderr) = $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'. + $packages = $this->fixturesDir . '/packages.json'; + $sut = $this->fixturesDir . '/create-project-test'; + $filesystem = new Filesystem(); + $filesystem->remove($sut); + list($stdout, $stderr) = $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, $stderr) = $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..149063c58a --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php @@ -0,0 +1,172 @@ +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(); + } + + /** + * Create a system-under-test and initialize a git repository for it. + */ + protected function createSutWithGit($topLevelProjectDir) { + $is_link = FALSE; + $this->fixturesDir = $this->fixtures->tmpDir($this->getName()); + $sut = $this->fixturesDir . '/' . $topLevelProjectDir; + $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 supress scaffolding. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $sut); + return $sut; + } + + /** + * Test to see if the scaffold operation 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)); + } + + /** + * Test to see if scaffold operation 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'); + } + + /** + * Test to see if the scaffold operation 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..f9c9e80fdf --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php @@ -0,0 +1,301 @@ +fileSystem = new Filesystem(); + $this->fixtures = new Fixtures(); + $this->projectRoot = $this->fixtures->projectRoot(); + $this->fixturesDir = getenv(self::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(); + } + + /** + * Create the System-Under-Test. + */ + protected function createSut($topLevelProjectDir, $replacements = []) { + $this->sut = $this->fixturesDir . '/' . $topLevelProjectDir; + // Erase just our sut, to ensure it is clean. Recopy all of the fixtures. + $this->fileSystem->remove($this->sut); + $replacements += ['PROJECT_ROOT' => $this->projectRoot]; + $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements); + return $this->sut; + } + + /** + * Data provider for testComposerInstallScaffold and testScaffoldCommand. + */ + public function scaffoldFixturesWithErrorConditionsTestValues() { + return [ + [ + 'drupal-drupal-missing-scaffold-file', + 'Scaffold file assets/missing-robots-default.txt not found in package fixtures/drupal-drupal-missing-scaffold-file.', + TRUE, + ], + ]; + } + + /** + * Tests that scaffold files throw when they have bad values. + * + * @dataProvider scaffoldFixturesWithErrorConditionsTestValues + */ + public function testScaffoldFixturesWithErrorConditions($topLevelProjectDir, $expectedExceptionMessage, $is_link) { + $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => $is_link ? 'true' : 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut); + // Test scaffold. Expect an error. + $this->expectException(\Exception::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $this->fixtures->runScaffold($sut); + } + + /** + * Data provider for testComposerInstallScaffold and testScaffoldCommand. + */ + public function scaffoldTestValues() { + return [ + [ + 'drupal-composer-drupal-project', + 'assertDrupalProjectSutWasScaffolded', TRUE, + ], + [ + 'drupal-drupal', + 'assertDrupalDrupalSutWasScaffolded', + FALSE, + ], + [ + 'drupal-drupal-test-overwrite', + 'assertDrupalDrupalFileWasReplaced', + FALSE, + ], + [ + 'drupal-drupal-test-append', + 'assertDrupalDrupalFileWasAppended', + FALSE, + ], + [ + 'drupal-drupal-test-append', + 'assertDrupalDrupalFileWasAppended', + TRUE, + ], + ]; + } + + /** + * Tests that scaffold files are correctly moved. + * + * @dataProvider scaffoldTestValues + */ + public function testScaffold($topLevelProjectDir, $scaffoldAssertions, $is_link) { + $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => $is_link ? 'true' : 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut); + // Test composer:scaffold. + $scaffoldOutput = $this->fixtures->runScaffold($sut); + // @todo We could assert that $scaffoldOutput must contain some expected text + call_user_func([$this, $scaffoldAssertions], $sut, $is_link, $topLevelProjectDir); + } + + /** + * Try to scaffold a project that does not scaffold anything. + */ + public function testEmptyProject() { + $topLevelProjectDir = 'empty-fixture'; + $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut); + // Test composer:scaffold. + $scaffoldOutput = $this->fixtures->runScaffold($sut); + $this->assertEquals('', $scaffoldOutput); + } + + /** + * Try to scaffold a project that allows a project with no scaffold files. + */ + public function testProjectThatScaffoldsEmptyProject() { + $topLevelProjectDir = 'project-allowing-empty-fixture'; + $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut); + // Test composer:scaffold. + $scaffoldOutput = $this->fixtures->runScaffold($sut); + $this->assertContains('The allowed package fixtures/empty-fixture does not provide a file mapping for Composer Scaffold', $scaffoldOutput); + $docroot = $sut; + $this->assertCommonDrupalAssetsWereScaffolded($docroot, FALSE, $topLevelProjectDir); + } + + /** + * Try to scaffold a project that attempts to scaffold a file with no path. + */ + public function testProjectWithEmptyScaffoldPath() { + $topLevelProjectDir = 'project-with-empty-scaffold-path'; + $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut); + // Test composer:scaffold. + $this->expectException(\Exception::class); + $this->expectExceptionMessage('No scaffold file path given for [web-root]/my-error in package fixtures/project-with-empty-scaffold-path'); + $this->fixtures->runScaffold($sut); + } + + /** + * Try to scaffold a project that attempts to scaffold a directory. + */ + public function testProjectWithIllegalDirScaffold() { + $topLevelProjectDir = 'project-with-illegal-dir-scaffold'; + $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']); + // Run composer install to get the dependencies we need to test. + $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut); + // Test composer:scaffold. + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Scaffold file assets in package fixtures/project-with-illegal-dir-scaffold is a directory; only files may be scaffolded'); + $this->fixtures->runScaffold($sut); + } + + /** + * Asserts that the drupal/assets scaffold files correct for drupal/project layout. + */ + protected function assertDrupalProjectSutWasScaffolded($sut, $is_link, $project_name) { + $docroot = $sut . '/docroot'; + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name); + $this->assertDefaultSettingsFromScaffoldOverride($docroot, $is_link); + $this->assertHtaccessExcluded($docroot); + } + + /** + * Asserts that the drupal/assets scaffold files correct for drupal/drupal layout. + */ + protected function assertDrupalDrupalSutWasScaffolded($sut, $is_link, $project_name) { + $docroot = $sut; + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name); + $this->assertDefaultSettingsFromScaffoldOverride($docroot, $is_link); + $this->assertHtaccessExcluded($docroot); + } + + /** + * Ensure that the default settings file was overridden by the test. + */ + protected function assertDefaultSettingsFromScaffoldOverride($docroot, $is_link) { + $this->assertScaffoldedFile($docroot . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#'); + } + + /** + * Ensure that the .htaccess file was excluded by the test. + */ + 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'); + } + + /** + * Assert 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. + */ + protected function assertDrupalDrupalFileWasReplaced($sut, $is_link, $project_name) { + $docroot = $sut; + $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, $project_name); + $this->assertScaffoldedFile($docroot . '/robots.txt', $is_link, "#{$project_name}#"); + } + + /** + * Confirm that the robots.txt file was prepended / appended as stipulated in the test. + */ + protected function assertDrupalDrupalFileWasAppended($sut, $is_link, $project_name) { + $docroot = $sut; + $this->assertScaffoldedFile($docroot . '/robots.txt', FALSE, '#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'); + $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name); + } + + /** + * Assert that the scaffold files from drupal/assets are placed as we expect them to be. + * + * 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. + */ + protected function assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name) { + $from_project = "#scaffolded from \"file-mappings\" in {$project_name} composer.json fixture#"; + $from_core = '#from drupal/core#'; + // Ensure that the autoload.php file was written. + $this->assertFileExists($docroot . '/autoload.php'); + // Assert other scaffold files are written in the correct locations. + $this->assertScaffoldedFile($docroot . '/.csslintrc', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/.editorconfig', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/.eslintignore', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/.eslintrc.json', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/.gitattributes', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/.ht.router.php', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/sites/default/default.services.yml', $is_link, $from_core); + $this->assertScaffoldedFile($docroot . '/sites/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..339daa0ae1 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php @@ -0,0 +1,62 @@ +destinationPath('[web-root]/robots.txt'); + $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt'); + $options = ScaffoldOptions::defaultOptions(); + $originalOp = new ReplaceOp(); + $originalOp->setSource($source); + $originalOp->setOverwrite(TRUE); + $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(); + $sut->setOriginalOp($originalOp); + $sut->setPrependFile($prepend); + $sut->setAppendFile($append); + // 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 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('Copy [web-root]/robots.txt from assets/robots.txt', $output); + $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..9ba1f92222 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php @@ -0,0 +1,82 @@ +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. + $sut->coalateScaffoldFiles($file_mappings, $locationReplacements); + $resolved_file_mappings = $sut->fileMappings(); + $scaffold_list = $sut->scaffoldList(); + // 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); + } + + /** + * Check 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. + */ + protected function assertResolvedToSameOp($project, $dest, $file_mappings, $scaffold_list, $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(); + $this->assertEquals(get_class($file_mappings[$project][$dest]), get_class($resolved_scaffold_op)); + $this->assertEquals($file_mappings[$project][$dest], $resolved_scaffold_op); + $this->assertEquals($project, $scaffold_list[$dest]->packageName()); + } + + /** + * Check 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. + */ + protected function assertOverridden($project, $dest, $scaffold_list, $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()); + } + +} 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..492b67ef20 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php @@ -0,0 +1,42 @@ +destinationPath('[web-root]/robots.txt'); + $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt'); + $options = ScaffoldOptions::defaultOptions(); + $sut = new ReplaceOp(); + $sut->setSource($source); + $sut->setOverwrite(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..fb600bd935 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php @@ -0,0 +1,37 @@ +destinationPath('[web-root]/robots.txt'); + $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt'); + $options = ScaffoldOptions::defaultOptions(); + $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/Unit/HandlerTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Unit/HandlerTest.php new file mode 100644 index 0000000000..881ddb1431 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Unit/HandlerTest.php @@ -0,0 +1,60 @@ +composer = $this->prophesize(Composer::class); + $this->io = $this->prophesize(IOInterface::class); + } + + /** + * @covers ::getWebRoot + */ + public function testGetWebRoot() { + $expected = './build/docroot'; + $extra = ['composer-scaffold' => ['locations' => ['web-root' => $expected]]]; + $package = $this->prophesize(PackageInterface::class); + $package->getExtra()->willReturn($extra); + $this->composer->getPackage()->willReturn($package->reveal()); + $fixture = new Handler($this->composer->reveal(), $this->io->reveal()); + $this->assertSame($expected, $fixture->getWebRoot()); + // Verify correct errors. + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The extra.composer-scaffold.location.web-root is not set in composer.json.'); + $extra = ['allowed-packages' => ['foo/bar']]; + $package->getExtra()->willReturn($extra); + $this->composer->getPackage()->willReturn($package->reveal()); + $fixture = new Handler($this->composer->reveal(), $this->io->reveal()); + $fixture->getWebRoot(); + } + +} 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..e103ffba5c --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md @@ -0,0 +1,34 @@ +# 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-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/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..f37a684279 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php @@ -0,0 +1,6 @@ +