├── AllowedPackages.php
├── CommandProvider.php
├── ComposerScaffoldCommand.php
├── GenerateAutoloadReferenceFile.php
├── Git.php
├── Handler.php
├── Interpolator.php
├── LICENSE.txt
├── ManageGitIgnore.php
├── ManageOptions.php
├── Operations
├── AbstractOperation.php
├── AppendOp.php
├── OperationData.php
├── OperationFactory.php
├── OperationInterface.php
├── ReplaceOp.php
├── ScaffoldFileCollection.php
├── ScaffoldResult.php
└── SkipOp.php
├── Plugin.php
├── PostPackageEventListenerInterface.php
├── README.md
├── ScaffoldFileInfo.php
├── ScaffoldFilePath.php
├── ScaffoldOptions.php
├── TESTING.txt
└── composer.json
/AllowedPackages.php:
--------------------------------------------------------------------------------
1 | composer = $composer;
60 | $this->io = $io;
61 | $this->manageOptions = $manage_options;
62 | }
63 |
64 | /**
65 | * Gets a list of all packages that are allowed to copy scaffold files.
66 | *
67 | * We will implicitly allow the projects 'drupal/legacy-scaffold-assets'
68 | * and 'drupal/core' to scaffold files, if they are present. Any other
69 | * project must be explicitly allowed in the top-level composer.json
70 | * file in order to be allowed to override scaffold files.
71 | * Configuration for packages specified later will override configuration
72 | * specified by packages listed earlier. In other words, the last listed
73 | * package has the highest priority. The root package will always be returned
74 | * at the end of the list.
75 | *
76 | * @return \Composer\Package\PackageInterface[]
77 | * An array of allowed Composer packages.
78 | */
79 | public function getAllowedPackages() {
80 | $top_level_packages = $this->getTopLevelAllowedPackages();
81 | $allowed_packages = $this->recursiveGetAllowedPackages($top_level_packages);
82 | // If the root package defines any file mappings, then implicitly add it
83 | // to the list of allowed packages. Add it at the end so that it overrides
84 | // all the preceding packages.
85 | if ($this->manageOptions->getOptions()->hasFileMapping()) {
86 | $root_package = $this->composer->getPackage();
87 | unset($allowed_packages[$root_package->getName()]);
88 | $allowed_packages[$root_package->getName()] = $root_package;
89 | }
90 | // Handle any newly-added packages that are not already allowed.
91 | return $this->evaluateNewPackages($allowed_packages);
92 | }
93 |
94 | /**
95 | * {@inheritdoc}
96 | */
97 | public function event(PackageEvent $event) {
98 | $operation = $event->getOperation();
99 | // Determine the package. Later, in evaluateNewPackages(), we will report
100 | // which of the newly-installed packages have scaffold operations, and
101 | // whether or not they are allowed to scaffold by the allowed-packages
102 | // option in the root-level composer.json file.
103 | $package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage();
104 | if (ScaffoldOptions::hasOptions($package->getExtra())) {
105 | $this->newPackages[$package->getName()] = $package;
106 | }
107 | }
108 |
109 | /**
110 | * Gets all packages that are allowed in the top-level composer.json.
111 | *
112 | * We will implicitly allow the projects 'drupal/legacy-scaffold-assets'
113 | * and 'drupal/core' to scaffold files, if they are present. Any other
114 | * project must be explicitly allowed in the top-level composer.json
115 | * file in order to be allowed to override scaffold files.
116 | *
117 | * @return array
118 | * An array of allowed Composer package names.
119 | */
120 | protected function getTopLevelAllowedPackages() {
121 | $implicit_packages = [
122 | 'drupal/legacy-scaffold-assets',
123 | 'drupal/core',
124 | ];
125 | $top_level_packages = $this->manageOptions->getOptions()->allowedPackages();
126 | return array_merge($implicit_packages, $top_level_packages);
127 | }
128 |
129 | /**
130 | * Builds a name-to-package mapping from a list of package names.
131 | *
132 | * @param string[] $packages_to_allow
133 | * List of package names to allow.
134 | * @param array $allowed_packages
135 | * Mapping of package names to PackageInterface of packages already
136 | * accumulated.
137 | *
138 | * @return \Composer\Package\PackageInterface[]
139 | * Mapping of package names to PackageInterface in priority order.
140 | */
141 | protected function recursiveGetAllowedPackages(array $packages_to_allow, array $allowed_packages = []) {
142 | foreach ($packages_to_allow as $name) {
143 | $package = $this->getPackage($name);
144 | if ($package instanceof PackageInterface && !isset($allowed_packages[$name])) {
145 | $allowed_packages[$name] = $package;
146 | $package_options = $this->manageOptions->packageOptions($package);
147 | $allowed_packages = $this->recursiveGetAllowedPackages($package_options->allowedPackages(), $allowed_packages);
148 | }
149 | }
150 | return $allowed_packages;
151 | }
152 |
153 | /**
154 | * Evaluates newly-added packages and see if they are already allowed.
155 | *
156 | * For now we will only emit warnings if they are not.
157 | *
158 | * @param array $allowed_packages
159 | * Mapping of package names to PackageInterface of packages already
160 | * accumulated.
161 | *
162 | * @return \Composer\Package\PackageInterface[]
163 | * Mapping of package names to PackageInterface in priority order.
164 | */
165 | protected function evaluateNewPackages(array $allowed_packages) {
166 | foreach ($this->newPackages as $name => $newPackage) {
167 | if (!array_key_exists($name, $allowed_packages)) {
168 | $this->io->write("Not scaffolding files for {$name}, because it is not listed in the element 'extra.drupal-scaffold.allowed-packages' in the root-level composer.json file.");
169 | }
170 | else {
171 | $this->io->write("Package {$name} has scaffold operations, and is already allowed in the root-level composer.json file.");
172 | }
173 | }
174 | // @todo We could prompt the user and ask if they wish to allow a
175 | // newly-added package. This might be useful if, for example, the user
176 | // might wish to require an installation profile that contains scaffolded
177 | // assets. For more information, see:
178 | // https://www.drupal.org/project/drupal/issues/3064990
179 | return $allowed_packages;
180 | }
181 |
182 | /**
183 | * Retrieves a package from the current composer process.
184 | *
185 | * @param string $name
186 | * Name of the package to get from the current composer installation.
187 | *
188 | * @return \Composer\Package\PackageInterface|null
189 | * The Composer package.
190 | */
191 | protected function getPackage($name) {
192 | return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
193 | }
194 |
195 | }
196 |
--------------------------------------------------------------------------------
/CommandProvider.php:
--------------------------------------------------------------------------------
1 | setName('drupal:scaffold')
25 | ->setAliases(['scaffold'])
26 | ->setDescription('Update the Drupal scaffold files.')
27 | ->setHelp(
28 | <<drupal:scaffold command places the scaffold files in their
30 | respective locations according to the layout stipulated in the composer.json
31 | file.
32 |
33 | php composer.phar drupal:scaffold
34 |
35 | It is usually not necessary to call drupal:scaffold manually,
36 | because it is called automatically as needed, e.g. after an install
37 | or update command. Note, though, that only packages explicitly
38 | allowed to scaffold in the top-level composer.json will be processed by this
39 | command.
40 |
41 | For more information, see https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold.
42 | EOT
43 | );
44 |
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | protected function execute(InputInterface $input, OutputInterface $output): int {
51 | $handler = new Handler($this->requireComposer(), $this->getIO());
52 | $handler->scaffold();
53 | return 0;
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/GenerateAutoloadReferenceFile.php:
--------------------------------------------------------------------------------
1 | findShortestPath($autoload_path->fullPath(), "$vendor/autoload.php");
47 | file_put_contents($autoload_path->fullPath(), static::autoLoadContents($relative_autoload_path));
48 | return new ScaffoldResult($autoload_path, TRUE);
49 | }
50 |
51 | /**
52 | * Determines whether or not the autoload file has been committed.
53 | *
54 | * @param \Composer\IO\IOInterface $io
55 | * IOInterface to write to.
56 | * @param string $package_name
57 | * The name of the package defining the autoload file (the root package).
58 | * @param string $web_root
59 | * The path to the web root.
60 | *
61 | * @return bool
62 | * True if autoload.php file exists and has been committed to the repository
63 | */
64 | public static function autoloadFileCommitted(IOInterface $io, $package_name, $web_root) {
65 | $autoload_path = static::autoloadPath($package_name, $web_root);
66 | $autoload_file = $autoload_path->fullPath();
67 | $location = dirname($autoload_file);
68 | if (!file_exists($autoload_file)) {
69 | return FALSE;
70 | }
71 | return Git::checkTracked($io, $autoload_file, $location);
72 | }
73 |
74 | /**
75 | * Generates a scaffold file path object for the autoload file.
76 | *
77 | * @param string $package_name
78 | * The name of the package defining the autoload file (the root package).
79 | * @param string $web_root
80 | * The path to the web root.
81 | *
82 | * @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
83 | * Object wrapping the relative and absolute path to the destination file.
84 | */
85 | protected static function autoloadPath($package_name, $web_root) {
86 | $rel_path = 'autoload.php';
87 | $dest_rel_path = '[web-root]/' . $rel_path;
88 | $dest_full_path = $web_root . '/' . $rel_path;
89 | return new ScaffoldFilePath('autoload', $package_name, $dest_rel_path, $dest_full_path);
90 | }
91 |
92 | /**
93 | * Builds the contents of the autoload file.
94 | *
95 | * @param string $relative_autoload_path
96 | * The relative path to the autoloader in vendor.
97 | *
98 | * @return string
99 | * Return the contents for the autoload.php.
100 | */
101 | protected static function autoLoadContents($relative_autoload_path) {
102 | $relative_autoload_path = preg_replace('#^\./#', '', $relative_autoload_path);
103 | return <<execute('git check-ignore ' . $process->escape($path), $output, $dir);
40 | return $exitCode == 0;
41 | }
42 |
43 | /**
44 | * Determines whether the specified scaffold file is tracked by git.
45 | *
46 | * @param \Composer\IO\IOInterface $io
47 | * The Composer IO interface.
48 | * @param string $path
49 | * Path to scaffold file to check.
50 | * @param string $dir
51 | * Base directory for git process.
52 | *
53 | * @return bool
54 | * Whether the specified file is already tracked or not (TRUE if tracked).
55 | */
56 | public static function checkTracked(IOInterface $io, $path, $dir = NULL) {
57 | $process = new ProcessExecutor($io);
58 | $output = '';
59 | $exitCode = $process->execute('git ls-files --error-unmatch ' . $process->escape($path), $output, $dir);
60 | return $exitCode == 0;
61 | }
62 |
63 | /**
64 | * Checks to see if the project root dir is in a git repository.
65 | *
66 | * @param \Composer\IO\IOInterface $io
67 | * The Composer IO interface.
68 | * @param string $dir
69 | * Base directory for git process.
70 | *
71 | * @return bool
72 | * True if this is a repository.
73 | */
74 | public static function isRepository(IOInterface $io, $dir = NULL) {
75 | $process = new ProcessExecutor($io);
76 | $output = '';
77 | $exitCode = $process->execute('git rev-parse --show-toplevel', $output, $dir);
78 | return $exitCode == 0;
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/Handler.php:
--------------------------------------------------------------------------------
1 | composer = $composer;
79 | $this->io = $io;
80 | $this->manageOptions = new ManageOptions($composer);
81 | $this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions);
82 | }
83 |
84 | /**
85 | * Registers post-package events if the 'require' command was called.
86 | */
87 | public function requireWasCalled() {
88 | // In order to differentiate between post-package events called after
89 | // 'composer require' vs. the same events called at other times, we will
90 | // only install our handler when a 'require' event is detected.
91 | $this->postPackageListeners[] = $this->manageAllowedPackages;
92 | }
93 |
94 | /**
95 | * Posts package command event.
96 | *
97 | * We want to detect packages 'require'd that have scaffold files, but are not
98 | * yet allowed in the top-level composer.json file.
99 | *
100 | * @param \Composer\Installer\PackageEvent $event
101 | * Composer package event sent on install/update/remove.
102 | */
103 | public function onPostPackageEvent(PackageEvent $event) {
104 | foreach ($this->postPackageListeners as $listener) {
105 | $listener->event($event);
106 | }
107 | }
108 |
109 | /**
110 | * Creates scaffold operation objects for all items in the file mappings.
111 | *
112 | * @param \Composer\Package\PackageInterface $package
113 | * The package that relative paths will be relative from.
114 | * @param array $package_file_mappings
115 | * The package file mappings array keyed by destination path and the values
116 | * are operation metadata arrays.
117 | *
118 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[]
119 | * A list of scaffolding operation objects
120 | */
121 | protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) {
122 | $scaffold_op_factory = new OperationFactory($this->composer);
123 | $scaffold_ops = [];
124 | foreach ($package_file_mappings as $dest_rel_path => $data) {
125 | $operation_data = new OperationData($dest_rel_path, $data);
126 | $scaffold_ops[$dest_rel_path] = $scaffold_op_factory->create($package, $operation_data);
127 | }
128 | return $scaffold_ops;
129 | }
130 |
131 | /**
132 | * Copies all scaffold files from source to destination.
133 | */
134 | public function scaffold() {
135 | // Recursively get the list of allowed packages. Only allowed packages
136 | // may declare scaffold files. Note that the top-level composer.json file
137 | // is implicitly allowed.
138 | $allowed_packages = $this->manageAllowedPackages->getAllowedPackages();
139 | if (empty($allowed_packages)) {
140 | $this->io->write("Nothing scaffolded because no packages are allowed in the top-level composer.json file.");
141 | return;
142 | }
143 |
144 | // Call any pre-scaffold scripts that may be defined.
145 | $dispatcher = $this->composer->getEventDispatcher();
146 | $dispatcher->dispatchScript(self::PRE_DRUPAL_SCAFFOLD_CMD);
147 |
148 | // Fetch the list of file mappings from each allowed package and normalize
149 | // them.
150 | $file_mappings = $this->getFileMappingsFromPackages($allowed_packages);
151 |
152 | $location_replacements = $this->manageOptions->getLocationReplacements();
153 | $scaffold_options = $this->manageOptions->getOptions();
154 |
155 | // Create a collection of scaffolded files to process. This determines which
156 | // take priority and which are combined.
157 | $scaffold_files = new ScaffoldFileCollection($file_mappings, $location_replacements);
158 |
159 | // Get the scaffold files whose contents on disk match what we are about to
160 | // write. We can remove these from consideration, as rewriting would be a
161 | // no-op.
162 | $unchanged = $scaffold_files->checkUnchanged();
163 | $scaffold_files->filterFiles($unchanged);
164 |
165 | // Process the list of scaffolded files.
166 | $scaffold_results = $scaffold_files->processScaffoldFiles($this->io, $scaffold_options);
167 |
168 | // Generate an autoload file in the document root that includes the
169 | // autoload.php file in the vendor directory, wherever that is. Drupal
170 | // requires this in order to easily locate relocated vendor dirs.
171 | $web_root = $this->manageOptions->getOptions()->getLocation('web-root');
172 | if (!GenerateAutoloadReferenceFile::autoloadFileCommitted($this->io, $this->rootPackageName(), $web_root)) {
173 | $scaffold_results[] = GenerateAutoloadReferenceFile::generateAutoload($this->io, $this->rootPackageName(), $web_root, $this->getVendorPath());
174 | }
175 |
176 | // Add the managed scaffold files to .gitignore if applicable.
177 | $gitIgnoreManager = new ManageGitIgnore($this->io, getcwd());
178 | $gitIgnoreManager->manageIgnored($scaffold_results, $scaffold_options);
179 |
180 | // Call post-scaffold scripts.
181 | $dispatcher->dispatchScript(self::POST_DRUPAL_SCAFFOLD_CMD);
182 | }
183 |
184 | /**
185 | * Gets the path to the 'vendor' directory.
186 | *
187 | * @return string
188 | * The file path of the vendor directory.
189 | */
190 | protected function getVendorPath() {
191 | $vendor_dir = $this->composer->getConfig()->get('vendor-dir');
192 | $filesystem = new Filesystem();
193 | return $filesystem->normalizePath(realpath($vendor_dir));
194 | }
195 |
196 | /**
197 | * Gets a consolidated list of file mappings from all allowed packages.
198 | *
199 | * @param \Composer\Package\PackageInterface[] $allowed_packages
200 | * A multidimensional array of file mappings, as returned by
201 | * self::getAllowedPackages().
202 | *
203 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[][]
204 | * An array of destination paths => scaffold operation objects.
205 | */
206 | protected function getFileMappingsFromPackages(array $allowed_packages) {
207 | $file_mappings = [];
208 | foreach ($allowed_packages as $package_name => $package) {
209 | $file_mappings[$package_name] = $this->getPackageFileMappings($package);
210 | }
211 | return $file_mappings;
212 | }
213 |
214 | /**
215 | * Gets the array of file mappings provided by a given package.
216 | *
217 | * @param \Composer\Package\PackageInterface $package
218 | * The Composer package from which to get the file mappings.
219 | *
220 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[]
221 | * An array of destination paths => scaffold operation objects.
222 | */
223 | protected function getPackageFileMappings(PackageInterface $package) {
224 | $options = $this->manageOptions->packageOptions($package);
225 | if ($options->hasFileMapping()) {
226 | return $this->createScaffoldOperations($package, $options->fileMapping());
227 | }
228 | // Warn the user if they allow a package that does not have any scaffold
229 | // files. We will ignore drupal/core, though, as it is implicitly allowed,
230 | // but might not have scaffold files (version 8.7.x and earlier).
231 | if (!$options->hasAllowedPackages() && ($package->getName() != 'drupal/core')) {
232 | $this->io->writeError("The allowed package {$package->getName()} does not provide a file mapping for Composer Scaffold.");
233 | }
234 | return [];
235 | }
236 |
237 | /**
238 | * Gets the root package name.
239 | *
240 | * @return string
241 | * The package name of the root project
242 | */
243 | protected function rootPackageName() {
244 | $root_package = $this->composer->getPackage();
245 | return $root_package->getName();
246 | }
247 |
248 | }
249 |
--------------------------------------------------------------------------------
/Interpolator.php:
--------------------------------------------------------------------------------
1 | startToken = $start_token;
43 | $this->endToken = $end_token;
44 | }
45 |
46 | /**
47 | * Sets the data set to use when interpolating.
48 | *
49 | * @param array $data
50 | * The key:value pairs to use when interpolating.
51 | *
52 | * @return $this
53 | */
54 | public function setData(array $data) {
55 | $this->data = $data;
56 | return $this;
57 | }
58 |
59 | /**
60 | * Adds to the data set to use when interpolating.
61 | *
62 | * @param array $data
63 | * The key:value pairs to use when interpolating.
64 | *
65 | * @return $this
66 | */
67 | public function addData(array $data) {
68 | $this->data = array_merge($this->data, $data);
69 | return $this;
70 | }
71 |
72 | /**
73 | * Replaces tokens in a string with values from an associative array.
74 | *
75 | * Tokens are surrounded by delimiters, e.g. square brackets "[key]". The
76 | * characters that surround the key may be defined when the Interpolator is
77 | * constructed.
78 | *
79 | * Example:
80 | * If the message is 'Hello, [user.name]', then the value of the user.name
81 | * item is fetched from the array, and the token [user.name] is replaced with
82 | * the result.
83 | *
84 | * @param string $message
85 | * Message containing tokens to be replaced.
86 | * @param array $extra
87 | * Data to use for interpolation in addition to whatever was provided to
88 | * self::setData().
89 | * @param string|bool $default
90 | * (optional) The value to substitute for tokens that are not found in the
91 | * data. If FALSE, then missing tokens are not replaced. Defaults to an
92 | * empty string.
93 | *
94 | * @return string
95 | * The message after replacements have been made.
96 | */
97 | public function interpolate($message, array $extra = [], $default = '') {
98 | $data = $extra + $this->data;
99 | $replacements = $this->replacements($message, $data, $default);
100 | return strtr($message, $replacements);
101 | }
102 |
103 | /**
104 | * Finds the tokens that exist in a message and builds a replacement array.
105 | *
106 | * All of the replacements in the data array are looked up given the token
107 | * keys from the provided message. Keys that do not exist in the configuration
108 | * are replaced with the default value.
109 | *
110 | * @param string $message
111 | * String with tokens.
112 | * @param array $data
113 | * Data to use for interpolation.
114 | * @param string $default
115 | * (optional) The value to substitute for tokens that are not found in the
116 | * data. If FALSE, then missing tokens are not replaced. Defaults to an
117 | * empty string.
118 | *
119 | * @return string[]
120 | * An array of replacements to make. Keyed by tokens and the replacements
121 | * are the values.
122 | */
123 | protected function replacements($message, array $data, $default = '') {
124 | $tokens = $this->findTokens($message);
125 | $replacements = [];
126 | foreach ($tokens as $sourceText => $key) {
127 | $replacement_text = array_key_exists($key, $data) ? $data[$key] : $default;
128 | if ($replacement_text !== FALSE) {
129 | $replacements[$sourceText] = $replacement_text;
130 | }
131 | }
132 | return $replacements;
133 | }
134 |
135 | /**
136 | * Finds all of the tokens in the provided message.
137 | *
138 | * @param string $message
139 | * String with tokens.
140 | *
141 | * @return string[]
142 | * map of token to key, e.g. {{key}} => key
143 | */
144 | protected function findTokens($message) {
145 | $reg_ex = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#';
146 | if (!preg_match_all($reg_ex, $message, $matches, PREG_SET_ORDER)) {
147 | return [];
148 | }
149 | $tokens = [];
150 | foreach ($matches as $matchSet) {
151 | [$sourceText, $key] = $matchSet;
152 | $tokens[$sourceText] = $key;
153 | }
154 | return $tokens;
155 | }
156 |
157 | }
158 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/ManageGitIgnore.php:
--------------------------------------------------------------------------------
1 | io = $io;
38 | $this->dir = $dir;
39 | }
40 |
41 | /**
42 | * Manages gitignore files.
43 | *
44 | * @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[] $files
45 | * A list of scaffold results, each of which holds a path and whether
46 | * or not that file is managed.
47 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
48 | * Configuration options from the composer.json extras section.
49 | */
50 | public function manageIgnored(array $files, ScaffoldOptions $options) {
51 | if (!$this->managementOfGitIgnoreEnabled($options)) {
52 | return;
53 | }
54 |
55 | // Accumulate entries to add to .gitignore, sorted into buckets based on the
56 | // location of the .gitignore file the entry should be added to.
57 | $add_to_git_ignore = [];
58 | foreach ($files as $scaffoldResult) {
59 | $path = $scaffoldResult->destination()->fullPath();
60 | $is_ignored = Git::checkIgnore($this->io, $path, $this->dir);
61 | if (!$is_ignored) {
62 | $is_tracked = Git::checkTracked($this->io, $path, $this->dir);
63 | if (!$is_tracked && $scaffoldResult->isManaged()) {
64 | $dir = realpath(dirname($path));
65 | $name = basename($path);
66 | $add_to_git_ignore[$dir][] = '/' . $name;
67 | }
68 | }
69 | }
70 | // Write out the .gitignore files one at a time.
71 | foreach ($add_to_git_ignore as $dir => $entries) {
72 | $this->addToGitIgnore($dir, $entries);
73 | }
74 | }
75 |
76 | /**
77 | * Determines whether we should manage gitignore files.
78 | *
79 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
80 | * Configuration options from the composer.json extras section.
81 | *
82 | * @return bool
83 | * Whether or not gitignore files should be managed.
84 | */
85 | protected function managementOfGitIgnoreEnabled(ScaffoldOptions $options) {
86 | // If the composer.json stipulates whether gitignore is managed or not, then
87 | // follow its recommendation.
88 | if ($options->hasGitIgnore()) {
89 | return $options->gitIgnore();
90 | }
91 |
92 | // Do not manage .gitignore if there is no repository here.
93 | if (!Git::isRepository($this->io, $this->dir)) {
94 | return FALSE;
95 | }
96 |
97 | // If the composer.json did not specify whether or not .gitignore files
98 | // should be managed, then manage them if the vendor directory is ignored.
99 | return Git::checkIgnore($this->io, 'vendor', $this->dir);
100 | }
101 |
102 | /**
103 | * Adds a set of entries to the specified .gitignore file.
104 | *
105 | * @param string $dir
106 | * Path to directory where gitignore should be written.
107 | * @param string[] $entries
108 | * Entries to write to .gitignore file.
109 | */
110 | protected function addToGitIgnore($dir, array $entries) {
111 | sort($entries);
112 | $git_ignore_path = $dir . '/.gitignore';
113 | $contents = '';
114 |
115 | // Appending to existing .gitignore files.
116 | if (file_exists($git_ignore_path)) {
117 | $contents = file_get_contents($git_ignore_path);
118 | if (!empty($contents) && !str_ends_with($contents, "\n")) {
119 | $contents .= "\n";
120 | }
121 | }
122 |
123 | $contents .= implode("\n", $entries);
124 | file_put_contents($git_ignore_path, $contents);
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/ManageOptions.php:
--------------------------------------------------------------------------------
1 | composer = $composer;
35 | }
36 |
37 | /**
38 | * Gets the root-level scaffold options for this project.
39 | *
40 | * @return \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions
41 | * The scaffold options object.
42 | */
43 | public function getOptions() {
44 | return $this->packageOptions($this->composer->getPackage());
45 | }
46 |
47 | /**
48 | * Gets the scaffold options for the stipulated project.
49 | *
50 | * @param \Composer\Package\PackageInterface $package
51 | * The package to fetch the scaffold options from.
52 | *
53 | * @return \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions
54 | * The scaffold options object.
55 | */
56 | public function packageOptions(PackageInterface $package) {
57 | return ScaffoldOptions::create($package->getExtra());
58 | }
59 |
60 | /**
61 | * Creates an interpolator for the 'locations' element.
62 | *
63 | * The interpolator returned will replace a path string with the tokens
64 | * defined in the 'locations' element.
65 | *
66 | * Note that only the root package may define locations.
67 | *
68 | * @return \Drupal\Composer\Plugin\Scaffold\Interpolator
69 | * Interpolator that will do replacements in a string using tokens in
70 | * 'locations' element.
71 | */
72 | public function getLocationReplacements() {
73 | return (new Interpolator())->setData($this->ensureLocations());
74 | }
75 |
76 | /**
77 | * Ensures that all of the locations defined in the scaffold files exist.
78 | *
79 | * Create them on the filesystem if they do not.
80 | */
81 | protected function ensureLocations() {
82 | $fs = new Filesystem();
83 | $locations = $this->getOptions()->locations() + ['web_root' => './'];
84 | $locations = array_map(function ($location) use ($fs) {
85 | $fs->ensureDirectoryExists($location);
86 | $location = realpath($location);
87 | return $location;
88 | }, $locations);
89 | return $locations;
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/Operations/AbstractOperation.php:
--------------------------------------------------------------------------------
1 | contents)) {
26 | $this->contents = $this->generateContents();
27 | }
28 | return $this->contents;
29 | }
30 |
31 | /**
32 | * Load the scaffold contents or otherwise generate what is needed.
33 | *
34 | * @return string
35 | * The contents of the scaffold file.
36 | */
37 | abstract protected function generateContents();
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function scaffoldOverExistingTarget(OperationInterface $existing_target) {
43 | return $this;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
50 | return $this;
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Operations/AppendOp.php:
--------------------------------------------------------------------------------
1 | forceAppend = $force_append;
82 | $this->prepend = $prepend_path;
83 | $this->append = $append_path;
84 | $this->default = $default_path;
85 | $this->managed = TRUE;
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | protected function generateContents() {
92 | // Fetch the prepend contents, if provided.
93 | $prepend_contents = '';
94 | if (!empty($this->prepend)) {
95 | $prepend_contents = file_get_contents($this->prepend->fullPath()) . "\n";
96 | }
97 | // Fetch the append contents, if provided.
98 | $append_contents = '';
99 | if (!empty($this->append)) {
100 | $append_contents = "\n" . file_get_contents($this->append->fullPath());
101 | }
102 |
103 | // Get the original contents, or the default data if the original is empty.
104 | $original_contents = $this->originalContents;
105 | if (empty($original_contents) && !empty($this->default)) {
106 | $original_contents = file_get_contents($this->default->fullPath());
107 | }
108 |
109 | // Attach it all together.
110 | return $prepend_contents . $original_contents . $append_contents;
111 | }
112 |
113 | /**
114 | * {@inheritdoc}
115 | */
116 | public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
117 | $destination_path = $destination->fullPath();
118 | $interpolator = $destination->getInterpolator();
119 |
120 | // Be extra-noisy of creating a new file or appending to a non-scaffold
121 | // file. Note that if the file already has the append contents, then the
122 | // OperationFactory will make a SkipOp instead, and we will not get here.
123 | if (!$this->managed) {
124 | $message = ' - NOTICE Modifying existing file at [dest-rel-path].';
125 | if (!file_exists($destination_path)) {
126 | $message = ' - NOTICE Creating a new file at [dest-rel-path].';
127 | }
128 | $message .= ' Examine the contents and ensure that it came out correctly.';
129 | $io->write($interpolator->interpolate($message));
130 | }
131 |
132 | // Notify that we are prepending, if there is prepend data.
133 | if (!empty($this->prepend)) {
134 | $this->prepend->addInterpolationData($interpolator, 'prepend');
135 | $io->write($interpolator->interpolate(" - Prepend to [dest-rel-path] from [prepend-rel-path]"));
136 | }
137 | // Notify that we are appending, if there is append data.
138 | if (!empty($this->append)) {
139 | $this->append->addInterpolationData($interpolator, 'append');
140 | $io->write($interpolator->interpolate(" - Append to [dest-rel-path] from [append-rel-path]"));
141 | }
142 |
143 | // Write the resulting data
144 | file_put_contents($destination_path, $this->contents());
145 |
146 | // Return a ScaffoldResult with knowledge of whether this file is managed.
147 | return new ScaffoldResult($destination, $this->managed);
148 | }
149 |
150 | /**
151 | * {@inheritdoc}
152 | */
153 | public function scaffoldOverExistingTarget(OperationInterface $existing_target) {
154 | $this->originalContents = $existing_target->contents();
155 | return $this;
156 | }
157 |
158 | /**
159 | * {@inheritdoc}
160 | */
161 | public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
162 | // If there is no existing scaffold file at the target location, then any
163 | // append we do will be to an unmanaged file.
164 | $this->managed = FALSE;
165 |
166 | // Default: do not allow an append over a file that was not scaffolded.
167 | if (!$this->forceAppend) {
168 | $message = " - Skip [dest-rel-path]: cannot append to a path that was not scaffolded unless 'force-append' property is set.";
169 | return new SkipOp($message);
170 | }
171 |
172 | // If the target file does not exist, then we will allow the append to
173 | // happen if we have default data to provide for it.
174 | if (!file_exists($destination->fullPath())) {
175 | if (!empty($this->default)) {
176 | return $this;
177 | }
178 | $message = " - Skip [dest-rel-path]: no file exists at the target path, and no default data provided.";
179 | return new SkipOp($message);
180 | }
181 |
182 | // If the target file DOES exist, and it already contains the append/prepend
183 | // data, then we will skip the operation.
184 | $existingData = file_get_contents($destination->fullPath());
185 | if ($this->existingFileHasData($existingData, $this->append) || $this->existingFileHasData($existingData, $this->prepend)) {
186 | $message = " - Skip [dest-rel-path]: the file already has the append/prepend data.";
187 | return new SkipOp($message);
188 | }
189 |
190 | // Cache the original data to use during append.
191 | $this->originalContents = $existingData;
192 |
193 | return $this;
194 | }
195 |
196 | /**
197 | * Check to see if the append/prepend data has already been applied.
198 | *
199 | * @param string $contents
200 | * The contents of the target file.
201 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $data_path
202 | * The path to the data to append or prepend.
203 | *
204 | * @return bool
205 | * 'TRUE' if the append/prepend data already exists in contents.
206 | */
207 | protected function existingFileHasData($contents, $data_path) {
208 | if (empty($data_path)) {
209 | return FALSE;
210 | }
211 | $data = file_get_contents($data_path->fullPath());
212 |
213 | return str_contains($contents, $data);
214 | }
215 |
216 | }
217 |
--------------------------------------------------------------------------------
/Operations/OperationData.php:
--------------------------------------------------------------------------------
1 | destination = $destination;
44 | $this->data = $this->normalizeScaffoldMetadata($destination, $data);
45 | }
46 |
47 | /**
48 | * Gets the destination path that this operation data is associated with.
49 | *
50 | * @return string
51 | * The destination path for the scaffold result.
52 | */
53 | public function destination() {
54 | return $this->destination;
55 | }
56 |
57 | /**
58 | * Gets operation mode.
59 | *
60 | * @return string
61 | * Operation mode.
62 | */
63 | public function mode() {
64 | return $this->data[self::MODE];
65 | }
66 |
67 | /**
68 | * Checks if path exists.
69 | *
70 | * @return bool
71 | * Returns true if path exists
72 | */
73 | public function hasPath() {
74 | return isset($this->data[self::PATH]);
75 | }
76 |
77 | /**
78 | * Gets path.
79 | *
80 | * @return string
81 | * The path.
82 | */
83 | public function path() {
84 | return $this->data[self::PATH];
85 | }
86 |
87 | /**
88 | * Determines overwrite.
89 | *
90 | * @return bool
91 | * Returns true if overwrite mode was selected.
92 | */
93 | public function overwrite() {
94 | return !empty($this->data[self::OVERWRITE]);
95 | }
96 |
97 | /**
98 | * Determines whether 'force-append' has been set.
99 | *
100 | * @return bool
101 | * Returns true if 'force-append' mode was selected.
102 | */
103 | public function forceAppend() {
104 | if ($this->hasDefault()) {
105 | return TRUE;
106 | }
107 | return !empty($this->data[self::FORCE_APPEND]);
108 | }
109 |
110 | /**
111 | * Checks if prepend path exists.
112 | *
113 | * @return bool
114 | * Returns true if prepend exists.
115 | */
116 | public function hasPrepend() {
117 | return isset($this->data[self::PREPEND]);
118 | }
119 |
120 | /**
121 | * Gets prepend path.
122 | *
123 | * @return string
124 | * Path to prepend data
125 | */
126 | public function prepend() {
127 | return $this->data[self::PREPEND];
128 | }
129 |
130 | /**
131 | * Checks if append path exists.
132 | *
133 | * @return bool
134 | * Returns true if prepend exists.
135 | */
136 | public function hasAppend() {
137 | return isset($this->data[self::APPEND]);
138 | }
139 |
140 | /**
141 | * Gets append path.
142 | *
143 | * @return string
144 | * Path to append data
145 | */
146 | public function append() {
147 | return $this->data[self::APPEND];
148 | }
149 |
150 | /**
151 | * Checks if default path exists.
152 | *
153 | * @return bool
154 | * Returns true if there is default data available.
155 | */
156 | public function hasDefault() {
157 | return isset($this->data[self::DEFAULT]);
158 | }
159 |
160 | /**
161 | * Gets default path.
162 | *
163 | * @return string
164 | * Path to default data
165 | */
166 | public function default() {
167 | return $this->data[self::DEFAULT];
168 | }
169 |
170 | /**
171 | * Normalizes metadata by converting literal values into arrays.
172 | *
173 | * Conversions performed include:
174 | * - Boolean 'false' means "skip".
175 | * - A string means "replace", with the string value becoming the path.
176 | *
177 | * @param string $destination
178 | * The destination path for the scaffold file.
179 | * @param mixed $value
180 | * The metadata for this operation object, which varies by operation type.
181 | *
182 | * @return array
183 | * Normalized scaffold metadata with default values.
184 | */
185 | protected function normalizeScaffoldMetadata($destination, $value) {
186 | $defaultScaffoldMetadata = [
187 | self::MODE => ReplaceOp::ID,
188 | self::PREPEND => NULL,
189 | self::APPEND => NULL,
190 | self::DEFAULT => NULL,
191 | self::OVERWRITE => TRUE,
192 | ];
193 |
194 | return $this->convertScaffoldMetadata($destination, $value) + $defaultScaffoldMetadata;
195 | }
196 |
197 | /**
198 | * Performs the conversion-to-array step in normalizeScaffoldMetadata.
199 | *
200 | * @param string $destination
201 | * The destination path for the scaffold file.
202 | * @param mixed $value
203 | * The metadata for this operation object, which varies by operation type.
204 | *
205 | * @return array
206 | * Normalized scaffold metadata.
207 | */
208 | protected function convertScaffoldMetadata($destination, $value) {
209 | if (is_bool($value)) {
210 | if (!$value) {
211 | return [self::MODE => SkipOp::ID];
212 | }
213 | throw new \RuntimeException("File mapping {$destination} cannot be given the value 'true'.");
214 | }
215 | if (empty($value)) {
216 | throw new \RuntimeException("File mapping {$destination} cannot be empty.");
217 | }
218 | if (is_string($value)) {
219 | $value = [self::PATH => $value];
220 | }
221 | // If there is no 'mode', but there is an 'append' or a 'prepend' path,
222 | // then the mode is 'append' (append + prepend).
223 | if (!isset($value[self::MODE]) && (isset($value[self::APPEND]) || isset($value[self::PREPEND]))) {
224 | $value[self::MODE] = AppendOp::ID;
225 | }
226 | return $value;
227 | }
228 |
229 | }
230 |
--------------------------------------------------------------------------------
/Operations/OperationFactory.php:
--------------------------------------------------------------------------------
1 | composer = $composer;
33 | }
34 |
35 | /**
36 | * Creates a scaffolding operation object as determined by the metadata.
37 | *
38 | * @param \Composer\Package\PackageInterface $package
39 | * The package that relative paths will be relative from.
40 | * @param OperationData $operation_data
41 | * The parameter data for this operation object; varies by operation type.
42 | *
43 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
44 | * The scaffolding operation object (skip, replace, etc.)
45 | *
46 | * @throws \RuntimeException
47 | * Exception thrown when parameter data does not identify a known scaffold
48 | * operation.
49 | */
50 | public function create(PackageInterface $package, OperationData $operation_data) {
51 | switch ($operation_data->mode()) {
52 | case SkipOp::ID:
53 | return new SkipOp();
54 |
55 | case ReplaceOp::ID:
56 | return $this->createReplaceOp($package, $operation_data);
57 |
58 | case AppendOp::ID:
59 | return $this->createAppendOp($package, $operation_data);
60 | }
61 | throw new \RuntimeException("Unknown scaffold operation mode {$operation_data->mode()}.");
62 | }
63 |
64 | /**
65 | * Creates a 'replace' scaffold op.
66 | *
67 | * Replace ops may copy or symlink, depending on settings.
68 | *
69 | * @param \Composer\Package\PackageInterface $package
70 | * The package that relative paths will be relative from.
71 | * @param OperationData $operation_data
72 | * The parameter data for this operation object, i.e. the relative 'path'.
73 | *
74 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
75 | * A scaffold replace operation object.
76 | */
77 | protected function createReplaceOp(PackageInterface $package, OperationData $operation_data) {
78 | if (!$operation_data->hasPath()) {
79 | throw new \RuntimeException("'path' component required for 'replace' operations.");
80 | }
81 | $package_name = $package->getName();
82 | $package_path = $this->getPackagePath($package);
83 | $source = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->path());
84 | $op = new ReplaceOp($source, $operation_data->overwrite());
85 | return $op;
86 | }
87 |
88 | /**
89 | * Creates an 'append' (or 'prepend') scaffold op.
90 | *
91 | * @param \Composer\Package\PackageInterface $package
92 | * The package that relative paths will be relative from.
93 | * @param OperationData $operation_data
94 | * The parameter data for this operation object, i.e. the relative 'path'.
95 | *
96 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
97 | * A scaffold replace operation object.
98 | */
99 | protected function createAppendOp(PackageInterface $package, OperationData $operation_data) {
100 | $package_name = $package->getName();
101 | $package_path = $this->getPackagePath($package);
102 | $prepend_source_file = NULL;
103 | $append_source_file = NULL;
104 | $default_data_file = NULL;
105 | if ($operation_data->hasPrepend()) {
106 | $prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->prepend());
107 | }
108 | if ($operation_data->hasAppend()) {
109 | $append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->append());
110 | }
111 | if ($operation_data->hasDefault()) {
112 | $default_data_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->default());
113 | }
114 | if (!$this->hasContent($prepend_source_file) && !$this->hasContent($append_source_file)) {
115 | $message = ' - Keep [dest-rel-path] unchanged: no content to prepend / append was provided.';
116 | return new SkipOp($message);
117 | }
118 |
119 | return new AppendOp($prepend_source_file, $append_source_file, $operation_data->forceAppend(), $default_data_file);
120 | }
121 |
122 | /**
123 | * Checks to see if the specified scaffold file exists and has content.
124 | *
125 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath|null $file
126 | * (optional) Scaffold file to check.
127 | *
128 | * @return bool
129 | * True if the file exists and has content.
130 | */
131 | protected function hasContent(?ScaffoldFilePath $file = NULL) {
132 | if (!$file) {
133 | return FALSE;
134 | }
135 | $path = $file->fullPath();
136 | return is_file($path) && (filesize($path) > 0);
137 | }
138 |
139 | /**
140 | * Gets the file path of a package.
141 | *
142 | * Note that if we call getInstallPath on the root package, we get the
143 | * wrong answer (the installation manager thinks our package is in
144 | * vendor). We therefore add special checking for this case.
145 | *
146 | * @param \Composer\Package\PackageInterface $package
147 | * The package.
148 | *
149 | * @return string
150 | * The file path.
151 | */
152 | protected function getPackagePath(PackageInterface $package) {
153 | if ($package->getName() == $this->composer->getPackage()->getName()) {
154 | // This will respect the --working-dir option if Composer is invoked with
155 | // it. There is no API or method to determine the filesystem path of
156 | // a package's composer.json file.
157 | return getcwd();
158 | }
159 | return $this->composer->getInstallationManager()->getInstallPath($package);
160 | }
161 |
162 | }
163 |
--------------------------------------------------------------------------------
/Operations/OperationInterface.php:
--------------------------------------------------------------------------------
1 | source = $sourcePath;
47 | $this->overwrite = $overwrite;
48 | }
49 |
50 | /**
51 | * {@inheritdoc}
52 | */
53 | protected function generateContents() {
54 | return file_get_contents($this->source->fullPath());
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
61 | $fs = new Filesystem();
62 | $destination_path = $destination->fullPath();
63 | // Do nothing if overwrite is 'false' and a file already exists at the
64 | // destination.
65 | if ($this->overwrite === FALSE && file_exists($destination_path)) {
66 | $interpolator = $destination->getInterpolator();
67 | $io->write($interpolator->interpolate(" - Skip [dest-rel-path] because it already exists and overwrite is false."));
68 | return new ScaffoldResult($destination, FALSE);
69 | }
70 |
71 | // Get rid of the destination if it exists, and make sure that
72 | // the directory where it's going to be placed exists.
73 | $fs->remove($destination_path);
74 | $fs->ensureDirectoryExists(dirname($destination_path));
75 | if ($options->symlink()) {
76 | return $this->symlinkScaffold($destination, $io);
77 | }
78 | return $this->copyScaffold($destination, $io);
79 | }
80 |
81 | /**
82 | * Copies the scaffold file.
83 | *
84 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
85 | * Scaffold file to process.
86 | * @param \Composer\IO\IOInterface $io
87 | * IOInterface to writing to.
88 | *
89 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
90 | * The scaffold result.
91 | */
92 | protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) {
93 | $interpolator = $destination->getInterpolator();
94 | $this->source->addInterpolationData($interpolator);
95 | if (file_put_contents($destination->fullPath(), $this->contents()) === FALSE) {
96 | throw new \RuntimeException($interpolator->interpolate("Could not copy source file [src-rel-path] to [dest-rel-path]!"));
97 | }
98 | $io->write($interpolator->interpolate(" - Copy [dest-rel-path] from [src-rel-path]"));
99 | return new ScaffoldResult($destination, $this->overwrite);
100 | }
101 |
102 | /**
103 | * Symlinks the scaffold file.
104 | *
105 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
106 | * Scaffold file to process.
107 | * @param \Composer\IO\IOInterface $io
108 | * IOInterface to writing to.
109 | *
110 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
111 | * The scaffold result.
112 | */
113 | protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) {
114 | $interpolator = $destination->getInterpolator();
115 | try {
116 | $fs = new Filesystem();
117 | $fs->relativeSymlink($this->source->fullPath(), $destination->fullPath());
118 | }
119 | catch (\Exception $e) {
120 | throw new \RuntimeException($interpolator->interpolate("Could not symlink source file [src-rel-path] to [dest-rel-path]!"), [], $e);
121 | }
122 | $io->write($interpolator->interpolate(" - Link [dest-rel-path] from [src-rel-path]"));
123 | return new ScaffoldResult($destination, $this->overwrite);
124 | }
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/Operations/ScaffoldFileCollection.php:
--------------------------------------------------------------------------------
1 | $package_file_mappings) {
46 | foreach ($package_file_mappings as $destination_rel_path => $op) {
47 | $destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements);
48 |
49 | // If there was already a scaffolding operation happening at this path,
50 | // allow the new operation to decide how to handle the override.
51 | // Usually, the new operation will replace whatever was there before.
52 | if (isset($scaffoldFiles[$destination_rel_path])) {
53 | $previous_scaffold_file = $scaffoldFiles[$destination_rel_path];
54 | $op = $op->scaffoldOverExistingTarget($previous_scaffold_file->op());
55 |
56 | // Remove the previous op so we only touch the destination once.
57 | $message = " - Skip [dest-rel-path]: overridden in {$package_name}";
58 | $this->scaffoldFilesByProject[$previous_scaffold_file->packageName()][$destination_rel_path] = new ScaffoldFileInfo($destination, new SkipOp($message));
59 | }
60 | // If there is NOT already a scaffolding operation happening at this
61 | // path, notify the scaffold operation of this fact.
62 | else {
63 | $op = $op->scaffoldAtNewLocation($destination);
64 | }
65 |
66 | // Combine the scaffold operation with the destination and record it.
67 | $scaffold_file = new ScaffoldFileInfo($destination, $op);
68 | $scaffoldFiles[$destination_rel_path] = $scaffold_file;
69 | $this->scaffoldFilesByProject[$package_name][$destination_rel_path] = $scaffold_file;
70 | }
71 | }
72 | }
73 |
74 | /**
75 | * Removes any item that has a path matching any path in the provided list.
76 | *
77 | * Matching is done via destination path.
78 | *
79 | * @param string[] $files_to_filter
80 | * List of destination paths.
81 | */
82 | public function filterFiles(array $files_to_filter) {
83 | foreach ($this->scaffoldFilesByProject as $project_name => $scaffold_files) {
84 | foreach ($scaffold_files as $destination_rel_path => $scaffold_file) {
85 | if (in_array($destination_rel_path, $files_to_filter, TRUE)) {
86 | unset($scaffold_files[$destination_rel_path]);
87 | }
88 | }
89 | $this->scaffoldFilesByProject[$project_name] = $scaffold_files;
90 | if (!$this->checkListHasItemWithContent($scaffold_files)) {
91 | unset($this->scaffoldFilesByProject[$project_name]);
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * Scans through a list of scaffold files and determines if any has contents.
98 | *
99 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo[] $scaffold_files
100 | * List of scaffold files, path: ScaffoldFileInfo.
101 | *
102 | * @return bool
103 | * TRUE if at least one item in the list has content
104 | */
105 | protected function checkListHasItemWithContent(array $scaffold_files) {
106 | foreach ($scaffold_files as $scaffold_file) {
107 | $contents = $scaffold_file->op()->contents();
108 | if (!empty($contents)) {
109 | return TRUE;
110 | }
111 | }
112 | return FALSE;
113 | }
114 |
115 | /**
116 | * {@inheritdoc}
117 | */
118 | public function getIterator(): \ArrayIterator {
119 | return new \ArrayIterator($this->scaffoldFilesByProject);
120 | }
121 |
122 | /**
123 | * Processes the files in our collection.
124 | *
125 | * @param \Composer\IO\IOInterface $io
126 | * The Composer IO object.
127 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $scaffold_options
128 | * The scaffold options.
129 | *
130 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[]
131 | * The results array.
132 | */
133 | public function processScaffoldFiles(IOInterface $io, ScaffoldOptions $scaffold_options) {
134 | $results = [];
135 | foreach ($this as $project_name => $scaffold_files) {
136 | $io->write("Scaffolding files for {$project_name}:");
137 | foreach ($scaffold_files as $scaffold_file) {
138 | $results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
139 | }
140 | }
141 | return $results;
142 | }
143 |
144 | /**
145 | * Processes the iterator created by ScaffoldFileCollection::create().
146 | *
147 | * @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection $collection
148 | * The iterator to process.
149 | * @param \Composer\IO\IOInterface $io
150 | * The Composer IO object.
151 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $scaffold_options
152 | * The scaffold options.
153 | *
154 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[]
155 | * The results array.
156 | *
157 | * @deprecated. Called when upgrading from the Core Composer Scaffold plugin
158 | * version 8.8.x due to a bug in the plugin and handler classes. Do not use
159 | * in 8.9.x or 9.x, and remove in Drupal 10.x.
160 | */
161 | public static function process(ScaffoldFileCollection $collection, IOInterface $io, ScaffoldOptions $scaffold_options) {
162 | $results = [];
163 | foreach ($collection as $project_name => $scaffold_files) {
164 | $io->write("Scaffolding files for {$project_name}:");
165 | foreach ($scaffold_files as $scaffold_file) {
166 | $results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
167 | }
168 | }
169 | return $results;
170 | }
171 |
172 | /**
173 | * Returns the list of files that have not changed since they were scaffolded.
174 | *
175 | * Note that there are two reasons a file may have changed:
176 | * - The user modified it after it was scaffolded.
177 | * - The package the file came to was updated, and the file is different in
178 | * the new version.
179 | *
180 | * With the current scaffold code, we cannot tell the difference between the
181 | * two. @see https://www.drupal.org/project/drupal/issues/3092563
182 | *
183 | * @return string[]
184 | * List of relative paths to unchanged files on disk.
185 | */
186 | public function checkUnchanged() {
187 | $results = [];
188 | foreach ($this as $scaffold_files) {
189 | foreach ($scaffold_files as $scaffold_file) {
190 | if (!$scaffold_file->hasChanged()) {
191 | $results[] = $scaffold_file->destination()->relativePath();
192 | }
193 | }
194 | }
195 | return $results;
196 | }
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/Operations/ScaffoldResult.php:
--------------------------------------------------------------------------------
1 | destination = $destination;
38 | $this->managed = $isManaged;
39 | }
40 |
41 | /**
42 | * Determines whether this scaffold file is managed.
43 | *
44 | * @return bool
45 | * TRUE if this scaffold file is managed, FALSE if not.
46 | */
47 | public function isManaged() {
48 | return $this->managed;
49 | }
50 |
51 | /**
52 | * Gets the destination scaffold file that this result refers to.
53 | *
54 | * @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
55 | * The destination path for the scaffold result.
56 | */
57 | public function destination() {
58 | return $this->destination;
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Operations/SkipOp.php:
--------------------------------------------------------------------------------
1 | [dest-rel-path]: disabled") {
35 | $this->message = $message;
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function generateContents() {
42 | return '';
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
49 | $interpolator = $destination->getInterpolator();
50 | $io->write($interpolator->interpolate($this->message));
51 | return new ScaffoldResult($destination, FALSE);
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Plugin.php:
--------------------------------------------------------------------------------
1 | composer = $composer;
59 | $this->io = $io;
60 | $this->requireWasCalled = FALSE;
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function deactivate(Composer $composer, IOInterface $io) {
67 | }
68 |
69 | /**
70 | * {@inheritdoc}
71 | */
72 | public function uninstall(Composer $composer, IOInterface $io) {
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function getCapabilities() {
79 | return [CommandProvider::class => ScaffoldCommandProvider::class];
80 | }
81 |
82 | /**
83 | * {@inheritdoc}
84 | */
85 | public static function getSubscribedEvents() {
86 | // Important note: We only instantiate our handler on "post" events.
87 | return [
88 | ScriptEvents::POST_UPDATE_CMD => 'postCmd',
89 | ScriptEvents::POST_INSTALL_CMD => 'postCmd',
90 | PackageEvents::POST_PACKAGE_INSTALL => 'postPackage',
91 | PluginEvents::COMMAND => 'onCommand',
92 | ];
93 | }
94 |
95 | /**
96 | * Post command event callback.
97 | *
98 | * @param \Composer\Script\Event $event
99 | * The Composer event.
100 | */
101 | public function postCmd(Event $event) {
102 | $this->handler()->scaffold();
103 | }
104 |
105 | /**
106 | * Post package event behavior.
107 | *
108 | * @param \Composer\Installer\PackageEvent $event
109 | * Composer package event sent on install/update/remove.
110 | */
111 | public function postPackage(PackageEvent $event) {
112 | $this->handler()->onPostPackageEvent($event);
113 | }
114 |
115 | /**
116 | * Pre command event callback.
117 | *
118 | * @param \Composer\Plugin\CommandEvent $event
119 | * The Composer command event.
120 | */
121 | public function onCommand(CommandEvent $event) {
122 | if ($event->getCommandName() == 'require') {
123 | if ($this->handler) {
124 | throw new \Error('Core Scaffold Plugin handler instantiated too early. See https://www.drupal.org/project/drupal/issues/3104922');
125 | }
126 | $this->requireWasCalled = TRUE;
127 | }
128 | }
129 |
130 | /**
131 | * Instantiates the handler object upon demand.
132 | *
133 | * It is dangerous to update a Composer plugin if it loads any classes prior
134 | * to the `composer update` operation, and later tries to use them in a
135 | * post-update hook.
136 | */
137 | protected function handler() {
138 | if (!$this->handler) {
139 | $this->handler = new Handler($this->composer, $this->io);
140 | // On instantiation of our handler, notify it if the 'require' command
141 | // was executed.
142 | if ($this->requireWasCalled) {
143 | $this->handler->requireWasCalled();
144 | }
145 | }
146 | return $this->handler;
147 | }
148 |
149 | }
150 |
--------------------------------------------------------------------------------
/PostPackageEventListenerInterface.php:
--------------------------------------------------------------------------------
1 | destination = $destination;
45 | $this->op = $op;
46 | }
47 |
48 | /**
49 | * Gets the Scaffold operation.
50 | *
51 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
52 | * Operations object that handles scaffolding (copy, make symlink, etc).
53 | */
54 | public function op() {
55 | return $this->op;
56 | }
57 |
58 | /**
59 | * Gets the package name.
60 | *
61 | * @return string
62 | * The name of the package this scaffold file info was collected from.
63 | */
64 | public function packageName() {
65 | return $this->destination->packageName();
66 | }
67 |
68 | /**
69 | * Gets the destination.
70 | *
71 | * @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
72 | * The scaffold path to the destination file.
73 | */
74 | public function destination() {
75 | return $this->destination;
76 | }
77 |
78 | /**
79 | * Determines if this scaffold file has been overridden by another package.
80 | *
81 | * @param string $providing_package
82 | * The name of the package that provides the scaffold file at this location,
83 | * as returned by self::findProvidingPackage()
84 | *
85 | * @return bool
86 | * Whether this scaffold file if overridden or removed.
87 | */
88 | public function overridden($providing_package) {
89 | return $this->packageName() !== $providing_package;
90 | }
91 |
92 | /**
93 | * Replaces placeholders in a message.
94 | *
95 | * @param string $message
96 | * Message with placeholders to fill in.
97 | * @param array $extra
98 | * Additional data to merge with the interpolator.
99 | * @param mixed $default
100 | * Default value to use for missing placeholders, or FALSE to keep them.
101 | *
102 | * @return string
103 | * Interpolated string with placeholders replaced.
104 | */
105 | public function interpolate($message, array $extra = [], $default = FALSE) {
106 | $interpolator = $this->destination->getInterpolator();
107 | return $interpolator->interpolate($message, $extra, $default);
108 | }
109 |
110 | /**
111 | * Moves a single scaffold file from source to destination.
112 | *
113 | * @param \Composer\IO\IOInterface $io
114 | * The scaffold file to be processed.
115 | * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
116 | * Assorted operational options, e.g. whether the destination should be a
117 | * symlink.
118 | *
119 | * @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
120 | * The scaffold result.
121 | */
122 | public function process(IOInterface $io, ScaffoldOptions $options) {
123 | return $this->op()->process($this->destination, $io, $options);
124 | }
125 |
126 | /**
127 | * Returns TRUE if the target does not exist or has changed.
128 | *
129 | * @return bool
130 | * TRUE if the target does not exist or has changed, FALSE otherwise.
131 | */
132 | final public function hasChanged() {
133 | $path = $this->destination()->fullPath();
134 | if (!file_exists($path)) {
135 | return TRUE;
136 | }
137 | return $this->op()->contents() !== file_get_contents($path);
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/ScaffoldFilePath.php:
--------------------------------------------------------------------------------
1 | type = $path_type;
65 | $this->packageName = $package_name;
66 | $this->relativePath = $rel_path;
67 | $this->fullPath = $full_path;
68 |
69 | // Ensure that the full path really is a full path. We do not use
70 | // 'realpath' here because the file specified by the full path might
71 | // not exist yet.
72 | $fs = new Filesystem();
73 | if (!$fs->isAbsolutePath($this->fullPath)) {
74 | $this->fullPath = getcwd() . '/' . $this->fullPath;
75 | }
76 | }
77 |
78 | /**
79 | * Gets the name of the package this source file was pulled from.
80 | *
81 | * @return string
82 | * Name of package.
83 | */
84 | public function packageName() {
85 | return $this->packageName;
86 | }
87 |
88 | /**
89 | * Gets the relative path to the source file (best to use in messages).
90 | *
91 | * @return string
92 | * Relative path to file.
93 | */
94 | public function relativePath() {
95 | return $this->relativePath;
96 | }
97 |
98 | /**
99 | * Gets the full path to the source file.
100 | *
101 | * @return string
102 | * Full path to file.
103 | */
104 | public function fullPath() {
105 | return $this->fullPath;
106 | }
107 |
108 | /**
109 | * Converts the relative source path into an absolute path.
110 | *
111 | * The path returned will be relative to the package installation location.
112 | *
113 | * @param string $package_name
114 | * The name of the package containing the source file. Only used for error
115 | * messages.
116 | * @param string $package_path
117 | * The installation path of the package containing the source file.
118 | * @param string $destination
119 | * Destination location provided as a relative path. Only used for error
120 | * messages.
121 | * @param string $source
122 | * Source location provided as a relative path.
123 | *
124 | * @return self
125 | * Object wrapping the relative and absolute path to the source file.
126 | */
127 | public static function sourcePath($package_name, $package_path, $destination, $source) {
128 | // Complain if there is no source path.
129 | if (empty($source)) {
130 | throw new \RuntimeException("No scaffold file path given for {$destination} in package {$package_name}.");
131 | }
132 | // Calculate the full path to the source scaffold file.
133 | $source_full_path = $package_path . '/' . $source;
134 | if (!file_exists($source_full_path)) {
135 | throw new \RuntimeException("Scaffold file {$source} not found in package {$package_name}.");
136 | }
137 | if (is_dir($source_full_path)) {
138 | throw new \RuntimeException("Scaffold file {$source} in package {$package_name} is a directory; only files may be scaffolded.");
139 | }
140 | return new self('src', $package_name, $source, $source_full_path);
141 | }
142 |
143 | /**
144 | * Converts the relative destination path into an absolute path.
145 | *
146 | * Any placeholders in the destination path, e.g. '[web-root]', will be
147 | * replaced using the provided location replacements interpolator.
148 | *
149 | * @param string $package_name
150 | * The name of the package defining the destination path.
151 | * @param string $destination
152 | * The relative path to the destination file being scaffolded.
153 | * @param \Drupal\Composer\Plugin\Scaffold\Interpolator $location_replacements
154 | * Interpolator that includes the [web-root] and any other available
155 | * placeholder replacements.
156 | *
157 | * @return self
158 | * Object wrapping the relative and absolute path to the destination file.
159 | */
160 | public static function destinationPath($package_name, $destination, Interpolator $location_replacements) {
161 | $dest_full_path = $location_replacements->interpolate($destination);
162 | return new self('dest', $package_name, $destination, $dest_full_path);
163 | }
164 |
165 | /**
166 | * Adds data about the relative and full path to the provided interpolator.
167 | *
168 | * @param \Drupal\Composer\Plugin\Scaffold\Interpolator $interpolator
169 | * Interpolator to add data to.
170 | * @param string $name_prefix
171 | * (optional) Prefix to add before -rel-path and -full-path item names.
172 | * Defaults to path type provided when constructing this object.
173 | */
174 | public function addInterpolationData(Interpolator $interpolator, $name_prefix = '') {
175 | if (empty($name_prefix)) {
176 | $name_prefix = $this->type;
177 | }
178 | $data = [
179 | 'package-name' => $this->packageName(),
180 | "{$name_prefix}-rel-path" => $this->relativePath(),
181 | "{$name_prefix}-full-path" => $this->fullPath(),
182 | ];
183 | $interpolator->addData($data);
184 | }
185 |
186 | /**
187 | * Interpolate a string using the data from this scaffold file info.
188 | *
189 | * @param string $name_prefix
190 | * (optional) Prefix to add before -rel-path and -full-path item names.
191 | * Defaults to path type provided when constructing this object.
192 | *
193 | * @return \Drupal\Composer\Plugin\Scaffold\Interpolator
194 | * An interpolator for making string replacements.
195 | */
196 | public function getInterpolator($name_prefix = '') {
197 | $interpolator = new Interpolator();
198 | $this->addInterpolationData($interpolator, $name_prefix);
199 | return $interpolator;
200 | }
201 |
202 | }
203 |
--------------------------------------------------------------------------------
/ScaffoldOptions.php:
--------------------------------------------------------------------------------
1 | options = $options + [
31 | "allowed-packages" => [],
32 | "locations" => [],
33 | "symlink" => FALSE,
34 | "file-mapping" => [],
35 | ];
36 |
37 | // Define any default locations.
38 | $this->options['locations'] += [
39 | 'project-root' => '.',
40 | 'web-root' => '.',
41 | ];
42 | }
43 |
44 | /**
45 | * Determines if the provided 'extras' section has scaffold options.
46 | *
47 | * @param array $extras
48 | * The contents of the 'extras' section.
49 | *
50 | * @return bool
51 | * True if scaffold options have been declared
52 | */
53 | public static function hasOptions(array $extras) {
54 | return array_key_exists('drupal-scaffold', $extras);
55 | }
56 |
57 | /**
58 | * Creates a scaffold options object.
59 | *
60 | * @param array $extras
61 | * The contents of the 'extras' section.
62 | *
63 | * @return self
64 | * The scaffold options object representing the provided scaffold options
65 | */
66 | public static function create(array $extras) {
67 | $options = static::hasOptions($extras) ? $extras['drupal-scaffold'] : [];
68 | return new self($options);
69 | }
70 |
71 | /**
72 | * Creates a new scaffold options object with some values overridden.
73 | *
74 | * @param array $options
75 | * Override values.
76 | *
77 | * @return self
78 | * The scaffold options object representing the provided scaffold options
79 | */
80 | protected function override(array $options) {
81 | return new self($options + $this->options);
82 | }
83 |
84 | /**
85 | * Creates a new scaffold options object with an overridden 'symlink' value.
86 | *
87 | * @param bool $symlink
88 | * Whether symlinking should be enabled or not.
89 | *
90 | * @return self
91 | * The scaffold options object representing the provided scaffold options
92 | */
93 | public function overrideSymlink($symlink) {
94 | return $this->override(['symlink' => $symlink]);
95 | }
96 |
97 | /**
98 | * Determines whether any allowed packages were defined.
99 | *
100 | * @return bool
101 | * Whether there are allowed packages
102 | */
103 | public function hasAllowedPackages() {
104 | return !empty($this->allowedPackages());
105 | }
106 |
107 | /**
108 | * Gets allowed packages from these options.
109 | *
110 | * @return array
111 | * The list of allowed packages
112 | */
113 | public function allowedPackages() {
114 | return $this->options['allowed-packages'];
115 | }
116 |
117 | /**
118 | * Gets the location mapping table, e.g. 'webroot' => './'.
119 | *
120 | * @return array
121 | * A map of name : location values
122 | */
123 | public function locations() {
124 | return $this->options['locations'];
125 | }
126 |
127 | /**
128 | * Determines whether a given named location is defined.
129 | *
130 | * @param string $name
131 | * The location name to search for.
132 | *
133 | * @return bool
134 | * True if the specified named location exist.
135 | */
136 | protected function hasLocation($name) {
137 | return array_key_exists($name, $this->locations());
138 | }
139 |
140 | /**
141 | * Gets a specific named location.
142 | *
143 | * @param string $name
144 | * The name of the location to fetch.
145 | *
146 | * @return string
147 | * The value of the provided named location
148 | */
149 | public function getLocation($name) {
150 | return $this->hasLocation($name) ? $this->locations()[$name] : FALSE;
151 | }
152 |
153 | /**
154 | * Determines if symlink mode is set.
155 | *
156 | * @return bool
157 | * Whether or not 'symlink' mode
158 | */
159 | public function symlink() {
160 | return $this->options['symlink'];
161 | }
162 |
163 | /**
164 | * Determines if there are file mappings.
165 | *
166 | * @return bool
167 | * Whether or not the scaffold options contain any file mappings
168 | */
169 | public function hasFileMapping() {
170 | return !empty($this->fileMapping());
171 | }
172 |
173 | /**
174 | * Returns the actual file mappings.
175 | *
176 | * @return array
177 | * File mappings for just this config type.
178 | */
179 | public function fileMapping() {
180 | return $this->options['file-mapping'];
181 | }
182 |
183 | /**
184 | * Determines if there is defined a value for the 'gitignore' option.
185 | *
186 | * @return bool
187 | * Whether or not there is a 'gitignore' option setting
188 | */
189 | public function hasGitIgnore() {
190 | return isset($this->options['gitignore']);
191 | }
192 |
193 | /**
194 | * Gets the value of the 'gitignore' option.
195 | *
196 | * @return bool
197 | * The 'gitignore' option, or TRUE if undefined.
198 | */
199 | public function gitIgnore() {
200 | return $this->hasGitIgnore() ? $this->options['gitignore'] : TRUE;
201 | }
202 |
203 | }
204 |
--------------------------------------------------------------------------------
/TESTING.txt:
--------------------------------------------------------------------------------
1 | HOW-TO: Test this Drupal composer plugin
2 |
3 | In order to test this plugin, you'll need to get the entire Drupal repo and
4 | run the tests there.
5 |
6 | You'll find the tests under core/tests/Drupal/Tests/Composer/Plugin.
7 |
8 | You can get the full Drupal repo here:
9 | https://www.drupal.org/project/drupal/git-instructions
10 |
11 | You can find more information about running PHPUnit tests with Drupal here:
12 | https://www.drupal.org/node/2116263
13 |
14 | Each component in the Drupal\Composer\Plugin namespace has its own annotated test
15 | group. You can use this group to run only the tests for this component. Like
16 | this:
17 |
18 | $ ./vendor/bin/phpunit -c core --group Scaffold
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "drupal/core-composer-scaffold",
3 | "description": "A flexible Composer project scaffold builder.",
4 | "type": "composer-plugin",
5 | "keywords": ["drupal"],
6 | "homepage": "https://www.drupal.org/project/drupal",
7 | "license": "GPL-2.0-or-later",
8 | "require": {
9 | "composer-plugin-api": "^2",
10 | "php": ">=7.3.0"
11 | },
12 | "conflict": {
13 | "drupal-composer/drupal-scaffold": "*"
14 | },
15 | "autoload": {
16 | "psr-4": {
17 | "Drupal\\Composer\\Plugin\\Scaffold\\": ""
18 | }
19 | },
20 | "extra": {
21 | "class": "Drupal\\Composer\\Plugin\\Scaffold\\Plugin",
22 | "branch-alias": {
23 | "dev-master": "1.0.x-dev"
24 | }
25 | },
26 | "config": {
27 | "sort-packages": true
28 | },
29 | "require-dev": {
30 | "composer/composer": "^1.8@stable"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------