devel_wizard-2.x-dev/src/Plugin/DevelWizard/Spell/ProjectDrupalDrushSpell.php
src/Plugin/DevelWizard/Spell/ProjectDrupalDrushSpell.php
<?php
declare(strict_types=1);
namespace Drupal\devel_wizard\Plugin\DevelWizard\Spell;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\devel_wizard\Attribute\DevelWizardSpell;
use Drupal\devel_wizard\ShellProcessFactoryInterface;
use Drupal\devel_wizard\Utils;
use Psr\Log\LoggerInterface;
use Sweetchuck\Utils\VersionNumber;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\String\UnicodeString;
#[DevelWizardSpell(
id: 'devel_wizard_project_drupal_drush',
category: new TranslatableMarkup('Project'),
label: new TranslatableMarkup('Project - drupal-drush'),
description: new TranslatableMarkup('Creates a new "drupal/foo" package with type "drupal-drush".'),
tags: [
'code' => new TranslatableMarkup('Code'),
'project' => new TranslatableMarkup('Project'),
'drush' => new TranslatableMarkup('Drush'),
],
)]
class ProjectDrupalDrushSpell extends SpellBase implements
PluginFormInterface,
ConfigurableInterface,
ContainerFactoryPluginInterface {
public const ERROR_CODE_GIT_INIT = 1;
protected ShellProcessFactoryInterface $processFactory;
protected Filesystem $fs;
protected TwigEnvironment $twig;
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger'),
$container->get('logger.channel.devel_wizard_spell'),
$container->get('string_translation'),
$container->get('devel_wizard.utils'),
$container->get('config.factory'),
$container->get('twig'),
$container->get('devel_wizard.shell_process_factory'),
new Filesystem(),
);
}
/**
* {@inheritdoc}
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
MessengerInterface $messenger,
LoggerInterface $logger,
TranslationInterface $stringTranslation,
Utils $utils,
ConfigFactoryInterface $configFactory,
TwigEnvironment $twig,
ShellProcessFactoryInterface $processFactory,
Filesystem $fs,
) {
$this->twig = $twig;
$this->processFactory = $processFactory;
$this->fs = $fs;
parent::__construct(
$configuration,
$plugin_id,
$plugin_definition,
$messenger,
$logger,
$stringTranslation,
$utils,
$configFactory,
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$coreVersionParts = explode('.', \Drupal::VERSION);
return [
'type' => 'custom',
'machine_name' => '',
'description' => '',
'php_min' => '8.1',
'core_compatibility' => [
'10' => $coreVersionParts[0] === '10',
'11' => $coreVersionParts[0] === '11',
'12' => $coreVersionParts[0] === '12',
],
'parent_dir' => '',
// @todo Prefix all the Git related configs with "git_".
'git_template' => 'default',
'initial_git_branch' => '1.x',
'add_git_branch_as_dir_suffix' => TRUE,
'phpstorm_idea_enabled' => FALSE,
'phpstorm_idea_' => [
'project_root' => '',
'project_name' => [],
'version_major' => 1,
],
];
}
public function populateCalculatedConfigurationValues(): static {
parent::populateCalculatedConfigurationValues();
$conf =& $this->configuration;
if (empty($conf['type'])) {
throw new \InvalidArgumentException('type is required');
}
if (empty($conf['machine_name'])) {
throw new \InvalidArgumentException('machine_name is required');
}
if (!$conf['parent_dir']) {
$conf['parent_dir'] = match($this->configuration['type']) {
'custom' => $this->getDefaultParentDirCustom(),
'standalone' => $this->getDefaultParentDirStandalone(),
};
}
$dirName = $conf['machine_name'];
if ($conf['type'] === 'standalone'
&& $conf['add_git_branch_as_dir_suffix']
&& !empty($conf['initial_git_branch'])
) {
$dirName .= "-{$conf['initial_git_branch']}";
}
$conf['dst_dir'] = Path::join($conf['parent_dir'], $dirName);
$version = VersionNumber::createFromString($conf['initial_git_branch']);
$conf['phpstorm_idea']['project_root'] = $conf['dst_dir'];
$conf['phpstorm_idea']['project_name'] = rtrim("drupal.{$conf['machine_name']}-{$version->major}", '-');
$conf['projectType'] = 'drupal-drush';
$conf['machineNameDash'] = str_replace('_', '-', $conf['machine_name']);
$conf['machineNameUpperCamel'] = (new UnicodeString("a_{$conf['machine_name']}"))
->camel()
->trimPrefix('a')
->toString();
return $this;
}
public function getDefaultParentDirCustom(): string {
return '../drush/Commands/custom';
}
public function getDefaultParentDirStandalone(): string {
// @todo Configurable standalone "drupal/*" projects directory.
return '../../../drupal';
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$parents = $form['#parents'];
$configuration = $this->getConfiguration();
$typeName = $this->utils->inputName($parents, 'type');
$statesTypeStandalone = [
'visible' => [
":input[name=\"$typeName\"]" => ['value' => 'standalone'],
],
];
$form['type'] = [
'#type' => 'radios',
'#required' => TRUE,
'#title' => $this->t('Type'),
'#options' => [
'custom' => $this->t('Custom'),
'standalone' => $this->t('Standalone'),
],
'#default_value' => $configuration['type'],
];
$form['machine_name'] = [
'#type' => 'textfield',
'#pattern' => '[a-z][a-z0-9_]{3,32}',
'#required' => TRUE,
'#title' => $this->t('Machine-name'),
'#description' => $this->t('Machine readable name of the new project'),
'#default_value' => $configuration['machine_name'],
];
$form['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Description'),
'#default_value' => $configuration['description'],
];
$form['php_min'] = [
'#type' => 'select',
'#title' => $this->t('Minimum PHP version'),
'#options' => $this->utils->phpVersionChoices(),
'#default_value' => $configuration['php_min'],
'#states' => $statesTypeStandalone,
];
$form['core_compatibility'] = [
'#type' => 'checkboxes',
'#title' => $this->t('drupal/core-recommended compatibility'),
'#description' => $this->t(
'Value for <code>@json_ref</code>',
[
'@json_ref' => '*.info.yml#/core_compatibility',
],
),
'#options' => [
'10' => '10',
'11' => '11',
'12' => '12',
],
'#default_value' => array_keys($configuration['core_compatibility'], TRUE, TRUE),
'#states' => $statesTypeStandalone,
];
$form['parent_dir'] = [
'#type' => 'textfield',
'#title' => $this->t('Parent directory'),
'#field_suffix' => '<code>/<machine_name>/<machine_name>.info.yml</code>',
'#default_value' => $configuration['parent_dir'],
'#description' => $this->t(
<<< 'TEXT'
Leave this empty to use the default value based on the type:
<ul>
<li>Sub-module: <code>@dir_submodule</code></li>
<li>Standalone: <code>@dir_standalone</code></li>
</ul>
TEXT,
[
'@dir_submodule' => $this->getDefaultParentDirCustom(),
'@dir_standalone' => $this->getDefaultParentDirStandalone(),
],
),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// @todo Implement validateConfigurationForm() method.
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValue($form['#parents'], []);
$values['core_compatibility'] = array_fill_keys(
array_keys($values['core_compatibility'], TRUE),
TRUE,
);
$this->setConfiguration($values);
}
/**
* @throws \Twig\Error\Error
*/
protected function doIt(): static {
$this
->doItDeployFiles()
->doItGit()
->doItPhpstorm();
drupal_flush_all_caches();
$this->batchContext['finished'] = 1.0;
return $this;
}
/**
* @throws \Twig\Error\Error
*/
protected function doItDeployFiles(): static {
$conf =& $this->configuration;
$files = [];
$files[Path::join($conf['dst_dir'], 'README.md')] = $this->getReadmeMdContent();
$files[Path::join($conf['dst_dir'], 'Commands', 'drush.yml')] = '{}';
if ($conf['type'] === 'standalone') {
$files[Path::join($conf['dst_dir'], 'composer.json')] = json_encode(
$this->getComposerJsonData(),
$this->utils->getJsonEncodeFlags(),
) . "\n";
$files[Path::join($conf['dst_dir'], '.editorconfig')] = $this->getEditorConfigContent();
$files[Path::join($conf['dst_dir'], '.gitignore')] = $this->getGitIgnoreContent();
$files[Path::join($conf['dst_dir'], 'phpcs.xml.dist')] = $this->getPhpcsXmlDistContent();
$files += $this->getPhpunitContents();
$files += $this->getPhpStanContents();
$files += $this->getRoboContents();
// @todo If not standalone and there is already PHPStan, PHPCS or PHPUnit
// configuration then maybe those are have to be adjusted.
}
$this->fs->mkdir(Path::join($conf['dst_dir'], 'Commands', $conf['machine_name']));
foreach ($files as $filePath => $fileContent) {
$this->fs->mkdir(Path::getDirectory($filePath));
$this->fs->dumpFile($filePath, $fileContent);
$this->messageFilesystemEntryCreate($filePath);
}
// PHPMD config.
// GitHubActions.
// CircleCI.
// GitLab.
// Jenkins.
return $this;
}
protected function doItGit(): static {
$conf =& $this->configuration;
$gitDir = Path::join(
$conf['dst_dir'],
'.git',
);
if ($this->fs->exists($gitDir)) {
$this->logger->info(
'git dir already exists: @git_dir',
[
'@git_dir' => $gitDir,
],
);
return $this;
}
$command = [
'git',
'init',
];
if ($conf['git_template']) {
$command[] = "--template={$conf['git_template']}";
}
if (!empty($conf['initial_git_branch'])) {
$command[] = "--initial-branch={$conf['initial_git_branch']}";
}
// @todo Remove \getenv().
$process = $this
->processFactory
->createInstance($command, $conf['dst_dir'], getenv());
$process->run();
if ($process->getExitCode() === 0) {
return $this;
}
// @todo Logger, messenger.
throw new \RuntimeException(
$process->getErrorOutput(),
static::ERROR_CODE_GIT_INIT,
);
}
/**
* @throws \Twig\Error\Error
*
* @todo Use devel_wizard_phpstorm_idea spell.
*/
protected function doItPhpstorm(): static {
$conf =& $this->configuration;
if (!$conf['phpstorm_idea_enabled']) {
return $this;
}
foreach ($this->getPhpstormContents() as $filePath => $fileContent) {
$this->fs->mkdir(Path::getDirectory($filePath));
$this->fs->dumpFile($filePath, $fileContent);
$this->messageFilesystemEntryCreate($filePath);
}
return $this;
}
/**
* @return array<string, string>
*
* @throws \Twig\Error\Error
*/
protected function getPhpstormContents(): array {
$conf =& $this->configuration;
$dstDir = Path::join($conf['dst_dir'], '.idea');
return [
"$dstDir/.name" => $conf['phpstorm_idea']['project_name'],
"$dstDir/inspectionProfiles/Project_Default.xml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/inspectionProfiles/Project_Default.xml.twig',
$conf,
),
"$dstDir/{$conf['phpstorm_idea']['project_name']}.iml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/PROJECT_NAME.iml.twig',
$conf,
),
"$dstDir/modules.xml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/modules.xml.twig',
$conf,
),
"$dstDir/php.xml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/php.xml.twig',
$conf,
),
"$dstDir/phpunit.xml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/phpunit.xml.twig',
$conf,
),
"$dstDir/vcs.xml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/vcs.xml.twig',
$conf,
),
"$dstDir/workspace.xml" => $this->twig->render(
'@devel_wizard/project-drupal-drush/idea/workspace.xml.twig',
$conf,
),
];
}
protected function getReadmeMdContent(): string {
return implode(
"\n",
[
"# drupal/{$this->configuration['machine_name']}",
'',
'@todo',
'',
],
);
}
protected function getComposerJsonData(): array {
$conf =& $this->configuration;
return [
'type' => 'drupal-drush',
'name' => "drupal/{$conf['machine_name']}",
'description' => $conf['description'] ?? '',
'license' => 'GPL-3.0-or-later',
'keywords' => [
'drupal',
'drush',
],
'homepage' => "https://drupal.org/project/{$conf['machine_name']}",
'support' => [
'issues' => "https://www.drupal.org/project/issues/{$conf['machine_name']}",
'source' => "https://git.drupalcode.org/project/{$conf['machine_name']}",
],
'minimum-stability' => 'dev',
'prefer-stable' => TRUE,
'config' => [
'allow-plugins' => [
'composer/installers' => TRUE,
'cweagans/composer-patches' => TRUE,
'dealerdirect/phpcodesniffer-composer-installer' => TRUE,
'drupal/core-composer-scaffold' => TRUE,
'oomphinc/composer-installers-extender' => TRUE,
],
'optimize-autoloader' => TRUE,
'preferred-install' => [
'*' => 'dist',
],
'sort-packages' => TRUE,
],
'require' => [
'php' => ">={$conf['php_min']}",
],
'require-dev' => [
'ext-pdo' => '*',
'ext-pdo_sqlite' => '*',
'behat/mink' => '^1.10',
'behat/mink-browserkit-driver' => '^2.1',
'composer/installers' => '^1.6',
'cweagans/composer-patches' => '^2.0',
'dealerdirect/phpcodesniffer-composer-installer' => '^1.0',
'drupal/core-composer-scaffold' => '^10.0',
'drupal/core-recommended' => '^10.0',
'drush/drush' => '^12.1',
'mikey179/vfsstream' => '^1.6',
'nuvoleweb/robo-config' => '^3.0',
'oomphinc/composer-installers-extender' => '^2.0',
'phpmd/phpmd' => '^2.13',
'phpspec/prophecy-phpunit' => '^2.0',
'phpunit/phpunit' => '^9.0',
'sweetchuck/robo-phpcs' => '3.x-dev',
'sweetchuck/robo-phpmd' => '3.x-dev',
'sweetchuck/robo-phpstan' => '2.x-dev',
'sweetchuck/utils' => '1.x-dev',
'symfony/browser-kit' => '^6.3',
'symfony/phpunit-bridge' => '^6.3',
'weitzman/drupal-test-traits' => '^2.1',
],
'suggest' => [
'drush/drush' => '^12.0',
],
'autoload' => [
'psr-4' => [
"Drush\\Commands\\{$conf['machine_name']}\\" => "Commands/{$conf['machine_name']}/",
"Drupal\\{$conf['machine_name']}\\" => 'src/',
],
],
'autoload-dev' => [
'psr-4' => [
"Drupal\\Tests\\{$conf['machine_name']}\\" => 'tests/src/',
'Drupal\\Tests\\' => 'tests/fixtures/repository/d9/project_01/docroot/core/tests/Drupal/Tests/',
'Drupal\\TestTools\\' => 'tests/fixtures/repository/d9/project_01/docroot/core/tests/Drupal/TestTools/',
'Drupal\\KernelTests\\' => 'tests/fixtures/repository/d9/project_01/docroot/core/tests/Drupal/KernelTests/',
],
],
'extra' => [
'drush' => [
'services' => [
'drush.services.yml' => '*',
],
],
'installer-types' => [
'bower-asset',
'npm-asset',
],
'installer-paths' => [
'tests/fixtures/repository/d9/project_01/docroot/core' => [
'type:drupal-core',
],
'tests/fixtures/repository/d9/project_01/docroot/libraries/{$name}' => [
'type:bower-asset',
'type:npm-asset',
'type:drupal-library',
],
'tests/fixtures/repository/d9/project_01/docroot/modules/contrib/{$name}' => [
'type:drupal-module',
],
'tests/fixtures/repository/d9/project_01/docroot/profiles/contrib/{$name}' => [
'type:drupal-profile',
],
'tests/fixtures/repository/d9/project_01/docroot/themes/contrib/{$name}' => [
'type:drupal-theme',
],
'tests/fixtures/repository/d9/project_01/drush/Commands/contrib/{$name}' => [
'type:drupal-drush',
],
],
'drupal-scaffold' => [
'locations' => [
'web-root' => 'tests/fixtures/repository/d9/project_01/docroot',
],
'file-mapping' => [
'[web-root]/modules/.gitignore' => [
'mode' => 'skip',
],
'[web-root]/modules/README.txt' => [
'mode' => 'skip',
],
'[web-root]/profiles/.gitignore' => [
'mode' => 'skip',
],
'[web-root]/profiles/README.txt' => [
'mode' => 'skip',
],
'[web-root]/themes/.gitignore' => [
'mode' => 'skip',
],
'[web-root]/themes/README.txt' => [
'mode' => 'skip',
],
'[web-root]/sites/example.settings.local.php' => [
'mode' => 'skip',
],
'[web-root]/sites/.gitignore' => [
'mode' => 'skip',
],
'[web-root]/sites/README.txt' => [
'mode' => 'skip',
],
'[web-root]/.csslintrc' => [
'mode' => 'skip',
],
'[web-root]/.editorconfig' => [
'mode' => 'skip',
],
'[web-root]/.eslintignore' => [
'mode' => 'skip',
],
'[web-root]/.eslintrc.json' => [
'mode' => 'skip',
],
'[web-root]/.gitattributes' => [
'mode' => 'skip',
],
'[web-root]/.gitignore' => [
'mode' => 'skip',
],
'[web-root]/example.gitignore' => [
'mode' => 'skip',
],
'[web-root]/INSTALL.txt' => [
'mode' => 'skip',
],
'[web-root]/README.txt' => [
'mode' => 'skip',
],
'[web-root]/.htaccess' => [
'mode' => 'skip',
],
'[web-root]/web.config' => [
'mode' => 'skip',
],
'[project-root]/.editorconfig' => [
'mode' => 'skip',
],
],
'initial' => [
'sites/default/default.services.yml' => 'sites/default/services.yml',
'sites/default/default.settings.php' => 'sites/default/settings.php',
],
],
$conf['machine_name'] => [
'fixtures' => [
'filesToSymlink' => [],
],
],
],
'scripts' => [
'post-install-cmd' => [
'@prepare',
],
'post-update-cmd' => [],
'clean' => [
'@clean:reports',
'@clean:composer',
],
'clean:reports' => [
'[ ! -d ./reports/ ] || find ./reports/ -mindepth 1 -maxdepth 1 -exec rm --recursive --force {} \;',
],
'clean:composer' => [
'rm --recursive --force ./vendor/',
'chmod --recursive u+w tests/',
'git clean --force -d -x -- ./tests/',
'git clean --force -d -X -- ./tests/',
],
'prepare' => [
'@prepare:phpunit-xml',
'@prepare:repository',
'@prepare:project',
],
'prepare:phpunit-xml' => [
"\\Drupal\\Tests\\{$conf['machine_name']}\\Helper\\ComposerScripts::cmdPreparePhpunitXml",
],
'prepare:repository' => [
"\\Drupal\\Tests\\{$conf['machine_name']}\\Helper\\ComposerScripts::cmdPrepareRepository",
],
'prepare:project' => [
"\\Drupal\\Tests\\{$conf['machine_name']}\\Helper\\ComposerScripts::cmdPrepareProject",
],
'lint' => [
'@lint:phpcs',
],
'lint:phpcs' => [
'phpcs',
],
'test' => [
'@clean:reports',
'@test:unit',
'@test:integration',
],
'test:unit' => [
'mkdir -p ./reports/machine/coverage-php/',
implode(' ', [
'phpunit',
'--testsuite="Unit"',
'--coverage-html ./reports/human/coverage/Unit/html',
'--testdox-html ./reports/human/testdox/Unit.html',
'--coverage-clover ./reports/machine/coverage-clover/Unit.xml',
'--coverage-php ./reports/machine/coverage-php/Unit.php',
'--log-junit ./reports/machine/junit/Unit.xml',
]),
],
'test:integration' => [
'mkdir -p ./reports/machine/coverage-php/',
implode(' ', [
'phpunit',
'--testsuite="Integration"',
'--coverage-html ./reports/human/coverage/Integration/html',
'--testdox-html ./reports/human/testdox/Integration.html',
'--coverage-clover ./reports/machine/coverage-clover/Integration.xml',
'--coverage-php ./reports/machine/coverage-php/Integration.php',
'--log-junit ./reports/machine/junit/Integration.xml',
]),
],
],
'scripts-descriptions' => [
'clean' => 'Clean third-party codes and generated reports.',
],
];
}
/**
* @throws \Twig\Error\Error
*/
protected function getEditorConfigContent(): string {
return $this->twig->render(
'@devel_wizard/project/editorconfig.twig',
$this->getConfiguration(),
);
}
/**
* @throws \Twig\Error\Error
*/
protected function getGitIgnoreContent(): string {
return $this->twig->render(
'@devel_wizard/project/gitignore.twig',
$this->getConfiguration(),
);
}
/**
* @throws \Twig\Error\Error
*/
protected function getPhpcsXmlDistContent(): string {
return $this->twig->render(
'@devel_wizard/project-drupal-drush/phpcs.xml.dist.twig',
$this->getConfiguration(),
);
}
/**
* @return array<string, string>
*
* @throws \Twig\Error\Error
*/
protected function getPhpStanContents(): array {
$conf =& $this->configuration;
$dstDir = $conf['dst_dir'];
return [
"$dstDir/phpstan.dist.neon" => $this->twig->render(
'@devel_wizard/project/phpstan.dist.neon.twig',
$conf,
),
"$dstDir/.phpstan/parameters.general.neon" => $this->twig->render(
'@devel_wizard/project/phpstan/parameters.general.neon.twig',
$conf,
),
"$dstDir/.phpstan/parameters.ignoreErrors.neon" => $this->twig->render(
'@devel_wizard/project/phpstan/parameters.ignoreErrors.neon.twig',
$conf,
),
"$dstDir/.phpstan/parameters.paths.neon" => $this->twig->render(
'@devel_wizard/project/phpstan/parameters.paths.neon.twig',
$conf,
),
"$dstDir/.phpstan/parameters.typeAliases.dev.neon" => $this->twig->render(
'@devel_wizard/project/phpstan/parameters.typeAliases.dev.neon.twig',
$conf,
),
"$dstDir/.phpstan/parameters.typeAliases.prod.neon" => $this->twig->render(
'@devel_wizard/project/phpstan/parameters.typeAliases.prod.neon.twig',
$conf,
),
];
}
/**
* @return array<string, string>
*
* @throws \Twig\Error\Error
*/
protected function getRoboContents(): array {
$dstDir = $this->configuration['dst_dir'];
return [
"$dstDir/RoboFile.php" => $this->twig->render(
'@devel_wizard/project-drupal-drush/RoboFile.php.twig',
$this->configuration,
),
"$dstDir/robo.yml.dist" => $this->twig->render(
'@devel_wizard/project/robo.yml.dist.twig',
$this->configuration,
),
];
}
/**
* @return array<string, string>
*
* @throws \Twig\Error\Error
*/
protected function getPhpunitContents(): array {
$dstDir = $this->configuration['dst_dir'];
return [
"$dstDir/phpunit.xml.dist" => $this->twig->render(
'@devel_wizard/project-drupal-drush/phpunit.xml.dist.twig',
$this->configuration,
),
"$dstDir/tests/src/bootstrap.php" => $this->twig->render(
'@devel_wizard/project-drupal-drush/tests/src/bootstrap.php.twig',
$this->configuration,
),
"$dstDir/tests/src/Helper/ComposerScripts.php" => $this->twig->render(
'@devel_wizard/project-drupal-drush/tests/src/Helper/ComposerScripts.php.twig',
$this->configuration,
),
];
}
}
