diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5c7df3f..d35b931 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -30,6 +30,12 @@ job-test: &job-test
sudo ldconfig
echo "export LD_LIBRARY_PATH=/usr/local/lib" >> $BASH_ENV
+ - run:
+ name: Setup composer.json
+ command: |
+ rm composer.json
+ mv composer.dist.json composer.json
+
- run:
name: Assemble the codebase
command: .devtools/assemble.sh
diff --git a/.devtools/start.sh b/.devtools/start.sh
index 7eb9505..0c9bcef 100755
--- a/.devtools/start.sh
+++ b/.devtools/start.sh
@@ -37,7 +37,8 @@ echo "==============================="
echo
info "Stopping previously started services, if any."
-killall -9 php >/dev/null 2>&1 || true
+# shellcheck disable=SC2009
+kill -SIGKILL "$(ps aux | grep 'php -S' | grep -v grep | awk '{print $2}')" >/dev/null 2>&1 || true
info "Starting the PHP webserver."
nohup php -S "${WEBSERVER_HOST}:${WEBSERVER_PORT}" -t "$(pwd)/build/web" "$(pwd)/build/web/.ht.router.php" >/tmp/php.log 2>&1 &
diff --git a/.devtools/stop.sh b/.devtools/stop.sh
index d629d6f..64cfdeb 100755
--- a/.devtools/stop.sh
+++ b/.devtools/stop.sh
@@ -24,7 +24,8 @@ echo "==============================="
echo
info "Stopping previously started services, if any."
-killall -9 php >/dev/null 2>&1 || true
+# shellcheck disable=SC2009
+kill -SIGKILL "$(ps aux | grep 'php -S' | grep -v grep | awk '{print $2}')" >/dev/null 2>&1 || true
sleep 1
pass "Services stopped."
diff --git a/.github/workflows/scaffold-test.yml b/.github/workflows/scaffold-test.yml
index 851fc35..0d28e58 100644
--- a/.github/workflows/scaffold-test.yml
+++ b/.github/workflows/scaffold-test.yml
@@ -56,14 +56,20 @@ jobs:
run: npm ci
working-directory: .scaffold/tests
+ - name: Install dependencies
+ run: composer install
+
- name: Run tests
- run: kcov --include-pattern=.sh,.bash --bash-parse-files-in-dir=. --exclude-pattern=vendor,node_modules,.scaffold-coverage-html,.scaffold "$(pwd)"/.scaffold-coverage-html .scaffold/tests/node_modules/.bin/bats .scaffold/tests/bats
+ run: |
+ kcov --include-pattern=.sh,.bash --bash-parse-files-in-dir=. --exclude-pattern=vendor,node_modules,.scaffold-coverage-html,.scaffold "$(pwd)"/.scaffold-coverage-html .scaffold/tests/node_modules/.bin/bats .scaffold/tests/bats
+ composer run lint
+ export XDEBUG_MODE='coverage' && composer run test
- name: Upload coverage report as an artifact
uses: actions/upload-artifact@v4
with:
name: ${{github.job}}-code-coverage-report
- path: ./.scaffold-coverage-html
+ path: ./.scaffold/.coverage-html
if-no-files-found: error
- name: Upload coverage report to Codecov
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e33d9da..5b40939 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -70,6 +70,11 @@ jobs:
php-version: ${{ matrix.php-version }}
extensions: gd, sqlite, pdo_sqlite
+ - name: Setup composer.json
+ run: |
+ rm composer.json
+ mv composer.dist.json composer.json
+
- name: Assemble the codebase
run: .devtools/assemble.sh
diff --git a/.scaffold/Customizer.php b/.scaffold/Customizer.php
new file mode 100644
index 0000000..0e7c565
--- /dev/null
+++ b/.scaffold/Customizer.php
@@ -0,0 +1,465 @@
+fileSystem = $filesystem;
+ $this->workingDir = $working_dir;
+ $this->io = $io;
+ $this->extensionName = $extension_name;
+ $this->extensionMachineName = $extension_machine_name;
+ $this->extensionType = $extension_type;
+ $this->ciProvider = $ci_provider;
+ $this->commandWrapper = $command_wrapper;
+ }
+
+ /**
+ * Process.
+ *
+ * @throws \Exception
+ */
+ public function process(): void {
+ // Display summary.
+ $this->displaySummary();
+ // Remove CI Provider.
+ $this->removeCiProvider();
+ // Remove command wrapper.
+ $this->removeCommandWrapper();
+ // Process README.
+ $this->processReadme();
+ // Process Composer.
+ $this->processComposer();
+ // Process internal replacement.
+ $this->processInternalReplacement();
+ }
+
+ /**
+ * Display summary input.
+ */
+ protected function displaySummary(): void {
+ $this->io->write(' Summary');
+ $this->io->write('---------------------------------');
+ $this->io->write('Name : ' . $this->extensionName);
+ $this->io->write('Machine name : ' . $this->extensionMachineName);
+ $this->io->write('Type : ' . $this->extensionType);
+ $this->io->write('CI Provider : ' . $this->ciProvider);
+ $this->io->write('Command wrapper : ' . $this->commandWrapper);
+ $this->io->write('Working dir : ' . $this->workingDir);
+ }
+
+ /**
+ * Process README.md.
+ */
+ protected function processReadme(): void {
+ $this->io->write('Processing README.');
+ $this->fileSystem->remove($this->workingDir . '/README.md');
+ $this->fileSystem->rename($this->workingDir . '/README.dist.md', $this->workingDir . '/README.md');
+ $logo_url = sprintf(
+ 'https://placehold.jp/000000/ffffff/200x200.png?text=%s&css=%s',
+ urlencode($this->extensionName),
+ urlencode('{"border-radius":" 100px"}'),
+ );
+ $logo_data = file_get_contents($logo_url);
+ if ($logo_data) {
+ file_put_contents($this->workingDir . '/logo.png', $logo_data);
+ }
+ }
+
+ /**
+ * Process composer scaffold.
+ */
+ protected function processComposer(): void {
+ $this->fileSystem->remove($this->workingDir . '/composer.json');
+ $this->fileSystem->remove($this->workingDir . '/composer.lock');
+ $this->fileSystem->remove($this->workingDir . '/vendor');
+ $this->fileSystem->rename($this->workingDir . '/composer.dist.json', $this->workingDir . '/composer.json');
+ }
+
+ /**
+ * Internal process to replace scaffold string and remove scaffold files.
+ *
+ * @throws \Exception
+ */
+ protected function processInternalReplacement(): void {
+ $this->io->write('Processing internal replacement.');
+
+ $extension_machine_name_class = self::convertString($this->extensionMachineName, 'class_name');
+
+ self::replaceStringInFilesInDirectory('YourExtension', $extension_machine_name_class, $this->workingDir);
+ self::replaceStringInFilesInDirectory('AlexSkrypnyk', $extension_machine_name_class, $this->workingDir);
+ self::replaceStringInFilesInDirectory('YourNamespace', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('yournamespace', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('alexskrypnyk', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('yourproject', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('Your+Extension', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('your_extension', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('drupal_extension_scaffold', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('[EXTENSION_NAME]', $this->extensionMachineName, $this->workingDir);
+ self::replaceStringInFilesInDirectory(
+ 'Provides your_extension functionality.',
+ sprintf('Provides %s functionality.', $this->extensionMachineName),
+ $this->workingDir,
+ );
+ self::replaceStringInFilesInDirectory(
+ 'Drupal module scaffold FE example used for template testing',
+ sprintf('Provides %s functionality.', $this->extensionMachineName),
+ $this->workingDir,
+ );
+ self::replaceStringInFilesInDirectory('Drupal extension scaffold', $this->extensionName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('Yourproject', $this->extensionName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('Your Extension', $this->extensionName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('your extension', $this->extensionName, $this->workingDir);
+ self::replaceStringInFilesInDirectory('drupal-module', 'drupal-' . $this->extensionType, $this->workingDir);
+ self::replaceStringInFilesInDirectory('type: module', 'type: ' . $this->extensionType, $this->workingDir);
+ self::replaceStringInFilesInDirectory('type: module', 'type: ' . $this->extensionType, $this->workingDir);
+
+ self::replaceStringInFile('# Uncomment the lines below in your project.', '', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# Remove the lines below in your project.', '', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('.github/FUNDING.yml export-ignore', '', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('LICENSE export-ignore', '', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .ahoy.yml export-ignore', '.ahoy.yml export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .circleci export-ignore', '.circleci export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .devtools export-ignore', '.devtools export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .editorconfig export-ignore', '.editorconfig export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .gitattributes export-ignore', '.gitattributes export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .github export-ignore', '.github export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .gitignore export-ignore', '.gitignore export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# .twig-cs-fixer.php export-ignore', '.twig-cs-fixer.php export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# Makefile export-ignore', 'Makefile export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# composer.dev.json export-ignore', 'composer.dev.json export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# phpcs.xml export-ignore', 'phpcs.xml export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# phpmd.xml export-ignore', 'phpmd.xml export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# phpstan.neon export-ignore', 'phpstan.neon export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# rector.php export-ignore', 'rector.php export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# renovate.json export-ignore', 'renovate.json export-ignore', $this->workingDir . '/.gitattributes');
+ self::replaceStringInFile('# tests export-ignore', 'tests export-ignore', $this->workingDir . '/.gitattributes');
+
+ $this->fileSystem->rename($this->workingDir . '/your_extension.info.yml', sprintf('%s/%s.info.yml', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/your_extension.install', sprintf('%s/%s.install', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/your_extension.links.menu.yml', sprintf('%s/%s.links.menu.yml', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/your_extension.module', sprintf('%s/%s.module', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/your_extension.routing.yml', sprintf('%s/%s.routing.yml', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/your_extension.services.yml', sprintf('%s/%s.services.yml', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/config/schema/your_extension.schema.yml', sprintf('%s/config/schema/%s.schema.yml', $this->workingDir, $this->extensionMachineName));
+ $this->fileSystem->rename($this->workingDir . '/src/Form/YourExtensionForm.php', sprintf('%s/src/Form/%sForm.php', $this->workingDir, $extension_machine_name_class));
+ $this->fileSystem->rename($this->workingDir . '/src/YourExtensionService.php', sprintf('%s/src/%sService.php', $this->workingDir, $extension_machine_name_class));
+ $this->fileSystem->rename($this->workingDir . '/tests/src/Unit/YourExtensionServiceUnitTest.php', sprintf('%s/tests/src/Unit/%sServiceUnitTest.php', $this->workingDir, $extension_machine_name_class));
+ $this->fileSystem->rename($this->workingDir . '/tests/src/Kernel/YourExtensionServiceKernelTest.php', sprintf('%s/tests/src/Kernel/%sServiceKernelTest.php', $this->workingDir, $extension_machine_name_class));
+ $this->fileSystem->rename($this->workingDir . '/tests/src/Functional/YourExtensionFunctionalTest.php', sprintf('%s/tests/src/Functional/%sFunctionalTest.php', $this->workingDir, $extension_machine_name_class));
+
+ $this->fileSystem->remove($this->workingDir . '/LICENSE');
+ $this->fileSystem->remove($this->workingDir . '/.scaffold');
+ $finder = Finder::create();
+ $finder
+ ->files()
+ ->in($this->workingDir . '/.github/workflows')
+ ->name('scaffold*.yml');
+ if ($finder->hasResults()) {
+ foreach ($finder as $file) {
+ $this->fileSystem->remove($file->getRealPath());
+ }
+ }
+
+ if ($this->extensionType === 'theme') {
+ $this->fileSystem->remove($this->workingDir . '/test');
+ $this->fileSystem->appendToFile(sprintf('%s/%s.info.yml', $this->workingDir, $this->extensionMachineName), 'base theme: false');
+ }
+ }
+
+ /**
+ * Remove CI provider depends on CI provider selected.
+ */
+ protected function removeCiProvider(): void {
+ $this->io->write('Processing remove CI Provider.');
+ $ci_provider = $this->ciProvider;
+ if ($ci_provider === 'gha') {
+ $this->removeCircleciCiProvider();
+ }
+ else {
+ $this->removeGhaCiProvider();
+ }
+ }
+
+ /**
+ * Remove CircleCi (circleci) CI provider.
+ */
+ protected function removeCircleciCiProvider(): void {
+ $this->fileSystem->remove($this->workingDir . '/.circleci');
+ }
+
+ /**
+ * Remove GitHub Action (gha) CI provider.
+ */
+ protected function removeGhaCiProvider(): void {
+ $this->fileSystem->remove($this->workingDir . '/.github/workflows');
+ }
+
+ /**
+ * Remove command wrappers depends on command wrapper selected.
+ */
+ protected function removeCommandWrapper(): void {
+ $this->io->write('Processing remove Command Wrapper.');
+ $command_wrapper = $this->commandWrapper;
+ switch ($command_wrapper) {
+ case 'ahoy':
+ $this->removeMakeCommandWrapper();
+ break;
+
+ case 'makefile':
+ $this->removeAhoyCommandWrapper();
+ break;
+
+ default:
+ $this->removeAhoyCommandWrapper();
+ $this->removeMakeCommandWrapper();
+ break;
+ }
+ }
+
+ /**
+ * Remove 'Ahoy' command wrapper.
+ */
+ protected function removeAhoyCommandWrapper(): void {
+ $this->fileSystem->remove($this->workingDir . '/.ahoy.yml');
+ }
+
+ /**
+ * Remove 'Make' command wrapper.
+ */
+ protected function removeMakeCommandWrapper(): void {
+ $this->fileSystem->remove($this->workingDir . '/Makefile');
+ }
+
+ /**
+ * @throws \Exception
+ *
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ */
+ public static function main(Event $event): void {
+ $io = $event->getIO();
+
+ $default_extension_name = getenv('DRUPAL_EXTENSION_SCAFFOLD_NAME') !== FALSE ? getenv('DRUPAL_EXTENSION_SCAFFOLD_NAME') : '';
+ $default_extension_type = getenv('DRUPAL_EXTENSION_SCAFFOLD_TYPE') !== FALSE ? getenv('DRUPAL_EXTENSION_SCAFFOLD_TYPE') : 'module';
+ $default_extension_ci_provider = getenv('DRUPAL_EXTENSION_SCAFFOLD_CI_PROVIDER') !== FALSE ? getenv('DRUPAL_EXTENSION_SCAFFOLD_CI_PROVIDER') : 'gha';
+ $default_extension_command_wrapper = getenv('DRUPAL_EXTENSION_SCAFFOLD_COMMAND_WRAPPER') !== FALSE ? getenv('DRUPAL_EXTENSION_SCAFFOLD_COMMAND_WRAPPER') : 'ahoy';
+
+ $io->write('Please follow the prompts to adjust your extension configuration');
+
+ $extension_name = $io->ask('Name: ', $default_extension_name);
+ $default_extension_machine_name = getenv('DRUPAL_EXTENSION_SCAFFOLD_MACHINE_NAME') !== FALSE ?
+ getenv('DRUPAL_EXTENSION_SCAFFOLD_MACHINE_NAME') : self::convertString($extension_name, 'file_name');
+ $extension_machine_name = $io->ask(sprintf('Machine Name: [%s]: ', $default_extension_machine_name), $default_extension_machine_name);
+ $extension_type = $io->ask(sprintf('Type: module or theme: [%s]: ', $default_extension_type), $default_extension_type);
+ $ci_provider = $io->ask(sprintf('CI Provider: GitHub Actions (gha) or CircleCI (circleci): [%s]: ', $default_extension_ci_provider), $default_extension_ci_provider);
+ $command_wrapper = $io->ask(sprintf('Command wrapper: Ahoy (ahoy), Makefile (makefile), None (none): [%s]: ', $default_extension_command_wrapper), $default_extension_command_wrapper);
+
+ if (!$extension_name) {
+ throw new \Exception('Name is required.');
+ }
+ if (!$extension_machine_name) {
+ throw new \Exception('Machine name is required.');
+ }
+ if (!in_array($extension_type, ['theme', 'module'])) {
+ throw new \Exception('Extension type is required or invalid.');
+ }
+ if (!in_array($ci_provider, ['gha', 'circleci'])) {
+ throw new \Exception('CI provider is required or invalid.');
+ }
+ if (!in_array($command_wrapper, ['ahoy', 'makefile', 'none'])) {
+ throw new \Exception('Command wrapper is required or invalid.');
+ }
+
+ $package_dir = $event->getComposer()
+ ->getInstallationManager()
+ ->getInstaller('project')
+ ->getInstallPath($event->getComposer()->getPackage());
+
+ $working_dir = Path::makeAbsolute('../../..', $package_dir);
+ $fileSystem = new Filesystem();
+
+ // @phpstan-ignore-next-line
+ $customizer = new static(
+ $fileSystem,
+ $io,
+ $working_dir,
+ $extension_name,
+ $extension_machine_name,
+ $extension_type,
+ $ci_provider,
+ $command_wrapper
+ );
+
+ try {
+ $customizer->process();
+ }
+ catch (\Exception $exception) {
+ throw new \Exception(sprintf('Initialization is not completed. Error %s', $exception->getMessage()), $exception->getCode(), $exception);
+ }
+
+ $io->write('Initialization complete.');
+ }
+
+ /**
+ * Convert a string to specific type.
+ *
+ * @throws \Exception
+ *
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ public static function convertString(string $string, string $type = 'file_name'): string {
+ switch ($type) {
+ case 'file_name':
+ $string_out = str_replace(' ', '_', $string);
+ $string_out = strtolower($string_out);
+ break;
+
+ case 'package_namespace':
+ $string_out = str_replace([' ', '-'], ['_', '_'], $string);
+ $string_out = strtolower($string_out);
+ break;
+
+ case 'namespace':
+ case 'class_name':
+ $string_out = str_replace(['-', '_'], [' ', ' '], $string);
+ $string_array = explode(' ', $string_out);
+ $new_string_array = [];
+ foreach ($string_array as $str) {
+ if (!empty(trim($str))) {
+ $new_string_array[] = ucfirst($str);
+ }
+ }
+ $string_out = implode('', $new_string_array);
+ break;
+
+ default:
+ throw new \Exception(sprintf('Convert string does not support type %s.', $type));
+ }
+
+ return $string_out;
+ }
+
+ /**
+ * Replace string in files in a directory.
+ *
+ * @param string|string[] $string_search
+ * String to search.
+ * @param string|string[] $string_replace
+ * String to replace.
+ * @param string $directory
+ * Directory.
+ */
+ public static function replaceStringInFilesInDirectory(string|array $string_search, string|array $string_replace, string $directory): void {
+ $finder = new Finder();
+ $finder
+ ->files()
+ ->contains($string_search)
+ ->in($directory);
+ if ($finder->hasResults()) {
+ foreach ($finder as $file) {
+ self::replaceStringInFile($string_search, $string_replace, $file->getRealPath());
+ }
+ }
+ }
+
+ /**
+ * Replace string in a file.
+ *
+ * @param string|string[] $string_search
+ * String to search.
+ * @param string|string[] $string_replace
+ * String to replace.
+ * @param string $file_path
+ * File path.
+ */
+ public static function replaceStringInFile(string|array $string_search, string|array $string_replace, string $file_path): void {
+ $file_content = file_get_contents($file_path);
+ if (!empty($file_content)) {
+ $new_file_content = str_replace($string_search, $string_replace, $file_content);
+ file_put_contents($file_path, $new_file_content);
+ }
+ }
+
+}
diff --git a/.scaffold/phpcs.xml b/.scaffold/phpcs.xml
new file mode 100644
index 0000000..89ecd1e
--- /dev/null
+++ b/.scaffold/phpcs.xml
@@ -0,0 +1,36 @@
+
+
+ Custom PHPCS standard.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customizer.php
+ tests/phpunit
+
diff --git a/.scaffold/phpmd.xml b/.scaffold/phpmd.xml
new file mode 100644
index 0000000..7d3cedb
--- /dev/null
+++ b/.scaffold/phpmd.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.scaffold/phpstan.neon b/.scaffold/phpstan.neon
new file mode 100644
index 0000000..419e1f5
--- /dev/null
+++ b/.scaffold/phpstan.neon
@@ -0,0 +1,23 @@
+##
+# Configuration file for PHPStan static code checking, see https://phpstan.org .
+#
+# Paths are passed as CLI arguments.
+
+parameters:
+
+ level: 7
+
+ paths:
+ - tests/phpunit
+ - Customizer.php
+
+ ignoreErrors:
+ -
+ # Hook implementations do not provide docblocks for parameters, so there
+ # is no way to provide this information.
+ messages:
+ - '#.* no value type specified in iterable type array#'
+ - '#.* has no return type specified#'
+ reportUnmatched: false
+
+ reportUnmatchedIgnoredErrors: false
diff --git a/.scaffold/phpunit.xml b/.scaffold/phpunit.xml
new file mode 100644
index 0000000..67db989
--- /dev/null
+++ b/.scaffold/phpunit.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ tests/phpunit
+
+
+
+
+ Customizer.php
+
+
+
+
+
+
+
+
+
diff --git a/.scaffold/rector.php b/.scaffold/rector.php
new file mode 100644
index 0000000..dfafeaf
--- /dev/null
+++ b/.scaffold/rector.php
@@ -0,0 +1,65 @@
+paths([
+ __DIR__ . '/**',
+ ]);
+
+ $rectorConfig->sets([
+ SetList::PHP_81,
+ SetList::PHP_82,
+ SetList::CODE_QUALITY,
+ SetList::CODING_STYLE,
+ SetList::DEAD_CODE,
+ SetList::INSTANCEOF,
+ SetList::TYPE_DECLARATION,
+ ]);
+
+ $rectorConfig->skip([
+ // Rules added by Rector's rule sets.
+ ArraySpreadInsteadOfArrayMergeRector::class,
+ CountArrayToEmptyArrayComparisonRector::class,
+ DisallowedEmptyRuleFixerRector::class,
+ InlineArrayReturnAssignRector::class,
+ NewlineAfterStatementRector::class,
+ NewlineBeforeNewAssignSetRector::class,
+ PostIncDecToPreIncDecRector::class,
+ RemoveAlwaysTrueIfConditionRector::class,
+ SimplifyEmptyCheckOnEmptyArrayRector::class,
+ // Dependencies.
+ '*/vendor/*',
+ '*/node_modules/*',
+ ]);
+
+ $rectorConfig->fileExtensions([
+ 'php',
+ 'inc',
+ ]);
+
+ $rectorConfig->importNames(TRUE, FALSE);
+ $rectorConfig->importShortClasses(FALSE);
+};
diff --git a/.scaffold/tests/bats/_assert_init.bash b/.scaffold/tests/bats/_assert_init.bash
deleted file mode 100644
index c2cb11a..0000000
--- a/.scaffold/tests/bats/_assert_init.bash
+++ /dev/null
@@ -1,187 +0,0 @@
-#!/usr/bin/env bash
-#
-# Scaffold template assertions.
-#
-
-# This file structure should exist in every project type.
-assert_files_present_common() {
- local dir="${1:-$(pwd)}"
-
- pushd "${dir}" >/dev/null || exit 1
-
- # Assert that some files must exist.
- assert_file_exists ".editorconfig"
- assert_file_exists ".gitattributes"
- assert_file_exists ".gitignore"
- assert_file_exists "README.md"
- assert_file_exists "composer.dev.json"
- assert_file_exists "composer.json"
- assert_file_exists "force_crystal.info.yml"
- assert_file_exists "logo.png"
- assert_file_exists "phpcs.xml"
- assert_file_exists "phpmd.xml"
- assert_file_exists "phpstan.neon"
- assert_file_exists "rector.php"
-
- # Assert that some files must not exist.
- assert_dir_not_exists ".scaffold"
- assert_file_not_exists ".github/workflows/scaffold-release.yml"
- assert_file_not_exists ".github/workflows/scaffold-test.yml"
- assert_file_not_exists "LICENSE"
- assert_file_not_exists "README.dist.md"
- assert_file_not_exists "logo.tmp.png"
-
- # Assert that .gitignore were processed correctly.
- assert_file_contains ".gitignore" "composer.lock"
- assert_file_contains ".gitignore" "build"
- assert_file_not_contains ".gitignore" "/coverage"
-
- # Assert that documentation was processed correctly.
- assert_file_not_contains README.md "META"
-
- # Assert that .gitattributes were processed correctly.
- assert_file_contains ".gitattributes" ".editorconfig"
- assert_file_not_contains ".gitattributes" "# .editorconfig"
- assert_file_contains ".gitattributes" ".gitattributes"
- assert_file_not_contains ".gitattributes" "# .gitattributes"
- assert_file_contains ".gitattributes" ".github"
- assert_file_not_contains ".gitattributes" "# .github"
- assert_file_contains ".gitattributes" ".gitignore"
- assert_file_not_contains ".gitattributes" "# .gitignore"
- assert_file_not_contains ".gitattributes" "# Uncomment the lines below in your project."
- assert_file_contains ".gitattributes" "tests"
- assert_file_contains ".gitattributes" "phpcs.xml"
- assert_file_contains ".gitattributes" "phpmd.xml"
- assert_file_contains ".gitattributes" "phpstan.neon"
- assert_file_not_contains ".gitattributes" "# tests"
- assert_file_not_contains ".gitattributes" "# phpcs.xml"
- assert_file_not_contains ".gitattributes" "# phpmd.xml"
- assert_file_not_contains ".gitattributes" "# phpstan.neon"
-
- # Assert that composer.json was processed correctly.
- assert_file_contains "composer.json" '"name": "drupal/force_crystal"'
- assert_file_contains "composer.json" '"description": "Provides force_crystal functionality."'
- assert_file_contains "composer.json" '"homepage": "https://drupal.org/project/force_crystal"'
- assert_file_contains "composer.json" '"issues": "https://drupal.org/project/issues/force_crystal"'
- assert_file_contains "composer.json" '"source": "https://git.drupalcode.org/project/force_crystal"'
-
- # Assert that extension info file was processed correctly.
- assert_file_contains "force_crystal.info.yml" 'name: Force Crystal'
-
- # Assert other things.
- assert_dir_not_contains_string "${dir}" "your_extension"
-
- popd >/dev/null || exit 1
-}
-
-assert_files_present_extension_type_module() {
- local dir="${1:-$(pwd)}"
-
- pushd "${dir}" >/dev/null || exit 1
-
- # Assert that extension info file were processed correctly.
- assert_file_contains "force_crystal.info.yml" 'type: module'
- assert_file_not_contains "force_crystal.info.yml" 'type: theme'
- assert_file_not_contains "force_crystal.info.yml" 'base theme: false'
-
- # Assert that composer.json file was processed correctly.
- assert_file_contains "composer.json" '"type": "drupal-module"'
-
- # Assert some dirs/files must exist.
- assert_dir_exists "tests/src/Unit"
- assert_dir_exists "tests/src/Functional"
-
- popd >/dev/null || exit 1
-}
-
-assert_files_present_extension_type_theme() {
- local dir="${1:-$(pwd)}"
-
- pushd "${dir}" >/dev/null || exit 1
-
- # Assert that extension info file were processed correctly.
- assert_file_contains "force_crystal.info.yml" 'type: theme'
- assert_file_contains "force_crystal.info.yml" 'base theme: false'
- assert_file_not_contains "force_crystal.info.yml" 'type: module'
-
- # Assert that composer.json file were processed correctly.
- assert_file_contains "composer.json" '"type": "drupal-theme"'
-
- # Assert some dirs/files must not exist.
- assert_dir_not_exists "tests/src/Unit"
- assert_dir_not_exists "tests/src/Functional"
-
- popd >/dev/null || exit 1
-}
-
-assert_ci_provider_circleci() {
- local dir="${1:-$(pwd)}"
- pushd "${dir}" >/dev/null || exit 1
-
- assert_file_exists ".circleci/config.yml"
-
- popd >/dev/null || exit 1
-}
-
-assert_ci_provider_gha() {
- local dir="${1:-$(pwd)}"
- pushd "${dir}" >/dev/null || exit 1
-
- assert_file_not_exists ".circleci/config.yml"
-
- popd >/dev/null || exit 1
-}
-
-assert_command_wrapper_ahoy() {
- local dir="${1:-$(pwd)}"
- pushd "${dir}" >/dev/null || exit 1
-
- assert_file_exists ".ahoy.yml"
- assert_file_not_exists "Makefile"
-
- popd >/dev/null || exit 1
-}
-
-assert_command_wrapper_makefile() {
- local dir="${1:-$(pwd)}"
- pushd "${dir}" >/dev/null || exit 1
-
- assert_file_not_exists ".ahoy.yml"
- assert_file_exists "Makefile"
-
- popd >/dev/null || exit 1
-}
-
-assert_command_wrapper_none() {
- local dir="${1:-$(pwd)}"
- pushd "${dir}" >/dev/null || exit 1
-
- assert_file_not_exists ".ahoy.yml"
- assert_file_not_exists "Makefile"
-
- popd >/dev/null || exit 1
-}
-
-assert_workflow_run() {
- local dir="${1:-$(pwd)}"
-
- pushd "${dir}" >/dev/null || exit 1
-
- ./.devtools/assemble.sh
- ./.devtools/start.sh
- ./.devtools/provision.sh
-
- pushd "build" >/dev/null || exit 1
-
- vendor/bin/phpcs
- vendor/bin/phpstan
- vendor/bin/rector --clear-cache --dry-run
- vendor/bin/phpmd . text phpmd.xml
- vendor/bin/twig-cs-fixer
-
- vendor/bin/phpunit
-
- popd >/dev/null || exit 1
-
- popd >/dev/null || exit 1
-}
diff --git a/.scaffold/tests/bats/functional.init.bats b/.scaffold/tests/bats/functional.init.bats
deleted file mode 100644
index b12cf5f..0000000
--- a/.scaffold/tests/bats/functional.init.bats
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/usr/bin/env bats
-#
-# Functional tests for init.sh.
-#
-# Example usage:
-# ./.scaffold/tests/node_modules/.bin/bats --no-tempdir-cleanup --formatter tap --filter-tags smoke .scaffold/tests
-#
-# shellcheck disable=SC2030,SC2031,SC2129
-
-load _helper
-load _assert_init
-
-export BATS_FIXTURE_EXPORT_CODEBASE_ENABLED=1
-export SCRIPT_FILE="init.sh"
-
-# bats test_tags=smoke
-@test "Init, defaults - extension module, workflow" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "module" # type
- "gha" # ci_provider
- "nothing" # remove init script
- "nothing" # command_wrapper
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_module "${BUILD_DIR}"
- assert_ci_provider_gha "${BUILD_DIR}"
- assert_command_wrapper_ahoy "${BUILD_DIR}"
- assert_output_contains "Initialization complete."
-
- assert_workflow_run "${BUILD_DIR}"
-}
-
-@test "Init, extension theme, workflow" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "theme" # type
- "gha" # ci_provider
- "nothing" # command_wrapper
- "nothing" # remove init script
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_theme "${BUILD_DIR}"
- assert_ci_provider_gha "${BUILD_DIR}"
- assert_command_wrapper_ahoy "${BUILD_DIR}"
- assert_output_contains "Initialization complete."
-
- assert_workflow_run "${BUILD_DIR}"
-}
-
-@test "Init, circleci" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "module" # type
- "circleci" # ci_provider
- "nothing" # command_wrapper
- "nothing" # remove init script
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_module "${BUILD_DIR}"
- assert_ci_provider_circleci "${BUILD_DIR}"
- assert_command_wrapper_ahoy "${BUILD_DIR}"
- assert_output_contains "Initialization complete."
-}
-
-@test "Init, Makefile" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "module" # type
- "circleci" # ci_provider
- "makefile" # command_wrapper
- "nothing" # remove init script
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_module "${BUILD_DIR}"
- assert_ci_provider_circleci "${BUILD_DIR}"
- assert_command_wrapper_makefile "${BUILD_DIR}"
- assert_output_contains "Initialization complete."
-}
-
-@test "Init, no command wrapper" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "module" # type
- "circleci" # ci_provider
- "none" # command_wrapper
- "nothing" # remove init script
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_module "${BUILD_DIR}"
- assert_ci_provider_circleci "${BUILD_DIR}"
- assert_command_wrapper_none "${BUILD_DIR}"
- assert_output_contains "Initialization complete."
-}
-
-@test "Init, do not remove script" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "module" # type
- "gha" # ci_provider
- "nothing" # command_wrapper
- "n" # remove init script
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_module "${BUILD_DIR}"
- assert_ci_provider_gha "${BUILD_DIR}"
- assert_command_wrapper_ahoy "${BUILD_DIR}"
- assert_file_exists "init.sh"
- assert_output_contains "Initialization complete."
-}
-
-@test "Init, remove script" {
- answers=(
- "Force Crystal" # name
- "force_crystal" # machine_name
- "module" # type
- "gha" # ci_provider
- "nothing" # command_wrapper
- "y" # remove init script
- "nothing" # proceed with init
- )
- tui_run "${answers[@]}"
-
- assert_output_contains "Please follow the prompts to adjust your extension configuration"
- assert_files_present_common "${BUILD_DIR}"
- assert_files_present_extension_type_module "${BUILD_DIR}"
- assert_ci_provider_gha "${BUILD_DIR}"
- assert_command_wrapper_ahoy "${BUILD_DIR}"
- assert_file_not_exists "init.sh"
- assert_output_contains "Initialization complete."
-}
diff --git a/.scaffold/tests/phpunit/Dirs.php b/.scaffold/tests/phpunit/Dirs.php
new file mode 100644
index 0000000..8160e6f
--- /dev/null
+++ b/.scaffold/tests/phpunit/Dirs.php
@@ -0,0 +1,73 @@
+fs = new Filesystem();
+ }
+
+ public function initLocations(): void {
+ $this->build = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'drevops-scaffold-' . microtime(TRUE);
+ $this->sut = $this->build . '/sut';
+ $this->repo = $this->build . '/local_repo';
+
+ $this->fs->mkdir($this->build);
+
+ $this->prepareLocalRepo();
+ }
+
+ public function deleteLocations(): void {
+ $this->fs->remove($this->build);
+ }
+
+ public function printInfo(): void {
+ $lines[] = '-- LOCATIONS --';
+ $lines[] = 'Build : ' . $this->build;
+ $lines[] = 'SUT : ' . $this->sut;
+ $lines[] = 'Local repo : ' . $this->repo;
+
+ fwrite(STDERR, PHP_EOL . implode(PHP_EOL, $lines) . PHP_EOL);
+ }
+
+ protected function prepareLocalRepo(): void {
+ $root = $this->fileFindDir('composer.json');
+
+ $this->fs->copy($root . '/composer.json', $this->repo . '/composer.json');
+ $this->fs->mirror($root . '/.devtools', $this->repo . '/.devtools');
+ $this->fs->mirror($root . '/.circleci', $this->repo . '/.circleci');
+
+ // Add the local repository to the composer.json file.
+ $composerjson = file_get_contents($this->repo . '/composer.json');
+ if ($composerjson === FALSE) {
+ throw new \Exception('Failed to read the local composer.json file.');
+ }
+
+ /** @var array $dst_json */
+ $dst_json = json_decode($composerjson, TRUE);
+ if (!$dst_json) {
+ throw new \Exception('Failed to decode the local composer.json file.');
+ }
+
+ $dst_json['repositories'][] = [
+ 'type' => 'path',
+ 'url' => $this->repo,
+ 'options' => [
+ 'symlink' => FALSE,
+ ],
+ ];
+ file_put_contents($this->repo . '/composer.json', json_encode($dst_json, JSON_PRETTY_PRINT));
+ }
+}
diff --git a/.scaffold/tests/phpunit/Functional/ScaffoldCreateProjectTest.php b/.scaffold/tests/phpunit/Functional/ScaffoldCreateProjectTest.php
new file mode 100644
index 0000000..5c08cbb
--- /dev/null
+++ b/.scaffold/tests/phpunit/Functional/ScaffoldCreateProjectTest.php
@@ -0,0 +1,16 @@
+assertEquals('b', 'b');
+ }
+
+ public function testCreateProjectInstall(): void {
+ $this->assertEquals('b', 'b');
+ }
+}
diff --git a/.scaffold/tests/phpunit/Functional/ScaffoldTestCase.php b/.scaffold/tests/phpunit/Functional/ScaffoldTestCase.php
new file mode 100644
index 0000000..fe4b9ea
--- /dev/null
+++ b/.scaffold/tests/phpunit/Functional/ScaffoldTestCase.php
@@ -0,0 +1,63 @@
+fs = new Filesystem();
+
+ $this->dirs = new Dirs();
+ $this->dirs->initLocations();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown(): void {
+ if (!$this->hasFailed()) {
+ $this->dirs->deleteLocations();
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function onNotSuccessfulTest(\Throwable $t): never {
+ $this->dirs->printInfo();
+
+ // Rethrow the exception to allow the test to fail normally.
+ parent::onNotSuccessfulTest($t);
+ }
+
+ public function hasFailed(): bool {
+ $status = $this->status();
+
+ return $status instanceof Failure;
+ }
+}
diff --git a/.scaffold/tests/phpunit/Traits/CmdTrait.php b/.scaffold/tests/phpunit/Traits/CmdTrait.php
new file mode 100644
index 0000000..8a1c6ab
--- /dev/null
+++ b/.scaffold/tests/phpunit/Traits/CmdTrait.php
@@ -0,0 +1,37 @@
+ getenv('PATH'), 'HOME' => getenv('HOME')];
+
+ $process = Process::fromShellCommandline($cmd, $cwd, $env);
+ $process->setTimeout(300)->setIdleTimeout(300)->run();
+
+ $exitCode = $process->getExitCode();
+ if (0 != $exitCode) {
+ throw new \RuntimeException("Exit code: {$exitCode}\n\n" . $process->getErrorOutput() . "\n\n" . $process->getOutput());
+ }
+
+ return $process->getOutput();
+ }
+}
diff --git a/.scaffold/tests/phpunit/Traits/ComposerTrait.php b/.scaffold/tests/phpunit/Traits/ComposerTrait.php
new file mode 100644
index 0000000..0cb538a
--- /dev/null
+++ b/.scaffold/tests/phpunit/Traits/ComposerTrait.php
@@ -0,0 +1,78 @@
+dirs->sut;
+ $args = implode(' ', $args);
+
+ return 'create-project --repository \'{"type": "path", "url": "' . $this->dirs->repo . '", "options": {"symlink": false}}\' alexskrypnyk/drupal_extension_scaffold="@dev" ' . $args;
+ }
+
+ /**
+ * Runs a `composer` command.
+ *
+ * @param string $cmd
+ * The Composer command to execute (escaped as required)
+ * @param string|null $cwd
+ * The current working directory to run the command from.
+ * @param array $env
+ * Environment variables to define for the subprocess.
+ *
+ * @return string
+ * Standard output and standard error from the command.
+ *
+ * @throws \Exception
+ */
+ public function composerRun(string $cmd, ?string $cwd = NULL, array $env = []): string {
+ $cwd = $cwd ?: $this->dirs->build;
+
+ $env += [
+ 'DREVOPS_SCAFFOLD_VERSION' => '@dev',
+ ];
+
+ $this->envFromInput($env);
+
+ chdir($cwd);
+
+ $input = new StringInput($cmd);
+ $output = new BufferedOutput();
+ // $output->setVerbosity(ConsoleOutput::VERBOSITY_QUIET);
+ $application = new Application();
+ $application->setAutoExit(FALSE);
+
+ $code = $application->run($input, $output);
+ $output = $output->fetch();
+
+ $this->envReset();
+
+ if ($code != 0) {
+ throw new \Exception("Fixtures::composerRun failed to set up fixtures.\n\nCommand: '{$cmd}'\nExit code: {$code}\nOutput: \n\n{$output}");
+ }
+
+ return $output;
+ }
+
+ protected function composerReadJson(?string $path = NULL): array {
+ $path = $path ?: $this->dirs->sut . '/composer.json';
+ $this->assertFileExists($path);
+
+ $composerjson = file_get_contents($path);
+ $this->assertIsString($composerjson);
+
+ $data = json_decode($composerjson, TRUE);
+ $this->assertIsArray($data);
+
+ return $data;
+ }
+}
diff --git a/.scaffold/tests/phpunit/Traits/EnvTrait.php b/.scaffold/tests/phpunit/Traits/EnvTrait.php
new file mode 100644
index 0000000..3091306
--- /dev/null
+++ b/.scaffold/tests/phpunit/Traits/EnvTrait.php
@@ -0,0 +1,93 @@
+ $value) {
+ static::envSet($name, $value);
+ if ($remove) {
+ unset($input[$name]);
+ }
+ }
+ }
+
+}
diff --git a/.scaffold/tests/phpunit/Traits/FileTrait.php b/.scaffold/tests/phpunit/Traits/FileTrait.php
new file mode 100644
index 0000000..ce48b87
--- /dev/null
+++ b/.scaffold/tests/phpunit/Traits/FileTrait.php
@@ -0,0 +1,29 @@
+exists($path)) {
+ return $current;
+ }
+ $current = dirname($current);
+ }
+
+ throw new \RuntimeException('File not found: ' . $file);
+ }
+}
diff --git a/.scaffold/tests/phpunit/Traits/JsonAssertTrait.php b/.scaffold/tests/phpunit/Traits/JsonAssertTrait.php
new file mode 100644
index 0000000..cfe87a1
--- /dev/null
+++ b/.scaffold/tests/phpunit/Traits/JsonAssertTrait.php
@@ -0,0 +1,22 @@
+find($path);
+
+ if (isset($result[0])) {
+ $this->fail($message ?: sprintf("The JSON path '%s' exists, but it was expected not to.", $path));
+ }
+
+ $this->addToAssertionCount(1);
+ }
+}
diff --git a/.scaffold/tests/phpunit/Unit/CustomizerTest.php b/.scaffold/tests/phpunit/Unit/CustomizerTest.php
new file mode 100644
index 0000000..df40055
--- /dev/null
+++ b/.scaffold/tests/phpunit/Unit/CustomizerTest.php
@@ -0,0 +1,169 @@
+filesystem = new Filesystem();
+ }
+
+ /**
+ * Test conver string.
+ *
+ * @param string $string_input
+ * String as input.
+ * @param string $convert_type
+ * Convert type.
+ * @param string|null $expected_string
+ * Expected string.
+ * @param bool $expected_sucess
+ * Expected fail or success.
+ */
+ #[DataProvider('convertStringProvider')]
+ public function testConvertString(string $string_input, string $convert_type, string|null $expected_string = NULL, bool $expected_sucess = TRUE): void {
+ if (!$expected_sucess) {
+ $this->expectException(\Exception::class);
+ }
+ $string_output = Customizer::convertString($string_input, $convert_type);
+ if ($expected_sucess) {
+ $this->assertEquals($expected_string, $string_output);
+ }
+ }
+
+ /**
+ * Data provider for convert string test.
+ */
+ public static function convertStringProvider(): array {
+ return [
+ 'test convert file_name' => ['This is-File_name TEST', 'file_name', 'this_is-file_name_test', TRUE],
+ 'test convert package_namespace' => ['This_is-Package_NAMESPACE TEST', 'package_namespace', 'this_is_package_namespace_test', TRUE],
+ 'test convert namespace' => ['This is-Namespace-_test', 'namespace', 'ThisIsNamespaceTest', TRUE],
+ 'test convert class_name' => ['This is-ClassName-_test', 'class_name', 'ThisIsClassNameTest', TRUE],
+ 'test convert dummy' => ['This is-CLassName-_TEST', 'dummy', NULL, FALSE],
+ ];
+ }
+
+ /**
+ * Test replace string in a file.
+ *
+ * @param string $string
+ * String, content in file before doing searching & replacment.
+ * @param string|string[] $string_search
+ * String to search.
+ * @param string|string[] $string_replace
+ * String to replace.
+ * @param string $string_expected
+ * Expected string after searching & replacment.
+ */
+ #[DataProvider('replaceStringInFileProvider')]
+ public function testReplaceStringInFile(string $string, string|array $string_search, string|array $string_replace, string $string_expected): void {
+ $file_path = tempnam(sys_get_temp_dir(), 'test-replace-string-');
+ if (!$file_path) {
+ throw new \Exception('Could not create test file: ' . $file_path);
+ }
+ $this->filesystem->dumpFile($file_path, $string);
+ $file_content = file_get_contents($file_path);
+ $this->assertEquals($string, $file_content);
+ Customizer::replaceStringInFile($string_search, $string_replace, $file_path);
+ $file_content = file_get_contents($file_path);
+ $this->assertEquals($string_expected, $file_content);
+ $this->filesystem->remove($file_path);
+ }
+
+ /**
+ * Data provider for test replace string in a file.
+ */
+ public static function replaceStringInFileProvider(): array {
+ return [
+ ['this text contains your-namespace-package', 'your-namespace-package', 'foo-package', 'this text contains foo-package'],
+ ['this text contains your-namespace-package', ['your-namespace-package'], ['foo-package'], 'this text contains foo-package'],
+ ['this text contains your-namespace-package', ['your-namespace'], ['foo-package'], 'this text contains foo-package-package'],
+ ['this text contains your-namespace-package', ['foo-your-namespace'], ['foo-package'], 'this text contains your-namespace-package']
+ ];
+ }
+
+ /**
+ * Test replace string in dir.
+ *
+ * @param string|string[] $string_search
+ * String to search.
+ * @param string|string[] $string_replace
+ * String to replace.
+ * @param string $directory
+ * Directory to search.
+ * @param array $files
+ * Files in above dir.
+ */
+ #[DataProvider('replaceStringInFilesInDirectoryProvider')]
+ public function testReplaceStringInFilesInDirectory(string|array $string_search, string|array $string_replace, string $directory, array $files): void {
+ $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $directory;
+
+ foreach ($files as $file) {
+ $file_path = $dir . DIRECTORY_SEPARATOR . $file['path'];
+ $this->filesystem->dumpFile($file_path, $file['content']);
+ $file_content = file_get_contents($file_path);
+ $this->assertEquals($file['content'], $file_content);
+ }
+
+ Customizer::replaceStringInFilesInDirectory($string_search, $string_replace, $dir);
+
+ foreach ($files as $file) {
+ $file_path = $dir . DIRECTORY_SEPARATOR . $file['path'];
+ $file_content = file_get_contents($file_path);
+ $this->assertEquals($file['expected_content'], $file_content);
+ }
+
+ $this->filesystem->remove($dir);
+ }
+
+ /**
+ * Data provider for test replace string in dir.
+ */
+ public static function replaceStringInFilesInDirectoryProvider(): array {
+ return [
+ [
+ 'search-string',
+ 'replace-string',
+ 'dir-1',
+ [
+ [
+ 'path' => 'foo/file-1.txt',
+ 'content' => 'Foo file 1 search-string content',
+ 'expected_content' => 'Foo file 1 replace-string content'
+ ],
+ [
+ 'path' => 'foo/file-2.txt',
+ 'content' => 'Foo file 2 search-string content',
+ 'expected_content' => 'Foo file 2 replace-string content'
+ ],
+ [
+ 'path' => 'foo/bar/file-1.txt',
+ 'content' => 'Foo/Bar file 1 content',
+ 'expected_content' => 'Foo/Bar file 1 content',
+ ],
+ ]
+ ],
+ ];
+ }
+
+}
diff --git a/composer.dist.json b/composer.dist.json
new file mode 100644
index 0000000..e162e99
--- /dev/null
+++ b/composer.dist.json
@@ -0,0 +1,28 @@
+{
+ "name": "drupal/your_extension",
+ "description": "Provides your_extension functionality.",
+ "type": "drupal-module",
+ "license": "GPL-2.0-or-later",
+ "authors": [
+ {
+ "name": "Your Name",
+ "email": "yourproject@yourusername.com",
+ "homepage": "https://www.drupal.org/u/yourusername",
+ "role": "Maintainer"
+ }
+ ],
+ "homepage": "https://drupal.org/project/your_extension",
+ "support": {
+ "issues": "https://drupal.org/project/issues/your_extension",
+ "source": "https://git.drupalcode.org/project/your_extension"
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "drupal/role_delegation": "~1"
+ },
+ "suggest": {
+ "drupal/config_ignore": "Ignore certain configuration during import."
+ }
+}
diff --git a/composer.json b/composer.json
index e162e99..d895e21 100644
--- a/composer.json
+++ b/composer.json
@@ -1,28 +1,60 @@
{
- "name": "drupal/your_extension",
- "description": "Provides your_extension functionality.",
- "type": "drupal-module",
+ "name": "alexskrypnyk/drupal_extension_scaffold",
+ "description": "Drupal extension scaffold.",
+ "type": "project",
"license": "GPL-2.0-or-later",
"authors": [
{
- "name": "Your Name",
- "email": "yourproject@yourusername.com",
- "homepage": "https://www.drupal.org/u/yourusername",
+ "name": "Alex Skrypnyk",
+ "email": "alex@drevops.com",
+ "homepage": "https://github.com/AlexSkrypnyk",
"role": "Maintainer"
}
],
- "homepage": "https://drupal.org/project/your_extension",
+ "homepage": "https://github.com/AlexSkrypnyk/drupal_extension_scaffold",
"support": {
- "issues": "https://drupal.org/project/issues/your_extension",
- "source": "https://git.drupalcode.org/project/your_extension"
+ "issues": "https://github.com/AlexSkrypnyk/drupal_extension_scaffold/issues",
+ "source": "https://github.com/AlexSkrypnyk/drupal_extension_scaffold"
},
"require": {
"php": ">=8.2"
},
+ "scripts": {
+ "post-create-project-cmd": [
+ "Scaffold\\Customizer::main"
+ ],
+ "lint": [
+ "cd .scaffold && ../vendor/bin/phpcs && ../vendor/bin/phpmd . text phpmd.xml && ../vendor/bin/phpstan && ../vendor/bin/rector --clear-cache --dry-run"
+ ],
+ "lint-fix": [
+ "cd .scaffold && ../vendor/bin/phpcbf && ../vendor/bin/rector --clear-cache"
+ ],
+ "test": [
+ "cd .scaffold && if [ \"${XDEBUG_MODE}\" = 'coverage' ]; then ../vendor/bin/phpunit; else ../vendor/bin/phpunit --no-coverage; fi"
+ ]
+ },
"require-dev": {
- "drupal/role_delegation": "~1"
+ "composer/composer": "^2.7",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "helmich/phpunit-json-assert": "^2.0",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpmd/phpmd": "^2.15",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^11.1",
+ "rector/rector": "^1.0",
+ "symfony/filesystem": "^6.0",
+ "symfony/finder": "^6.0",
+ "symfony/process": "^6.4"
+ },
+ "autoload":{
+ "psr-4":{
+ "Scaffold\\": ".scaffold",
+ "Scaffold\\Tests\\": ".scaffold/tests/phpunit"
+ }
},
- "suggest": {
- "drupal/config_ignore": "Ignore certain configuration during import."
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
}
}
diff --git a/init.sh b/init.sh
deleted file mode 100755
index deca3f6..0000000
--- a/init.sh
+++ /dev/null
@@ -1,300 +0,0 @@
-#!/usr/bin/env bash
-##
-# Adjust project repository based on user input.
-#
-# @usage:
-# Interactive prompt:
-# ./init.sh
-#
-# Silent:
-# ./init.sh "Extension Name" extension_machine_name extension_type ci_provider command_wrapper
-#
-# shellcheck disable=SC2162,SC2015
-
-set -euo pipefail
-[ "${SCRIPT_DEBUG-}" = "1" ] && set -x
-
-extension_name=${1-}
-extension_machine_name=${2-}
-extension_type=${3-}
-ci_provider=${4-}
-command_wrapper=${5-}
-
-#-------------------------------------------------------------------------------
-
-convert_string() {
- input_string="$1"
- conversion_type="$2"
-
- case "${conversion_type}" in
- "file_name" | "route_path" | "deployment_id")
- echo "${input_string}" | tr ' ' '_' | tr '[:upper:]' '[:lower:]'
- ;;
- "domain_name" | "package_namespace")
- echo "${input_string}" | tr ' ' '_' | tr '[:upper:]' '[:lower:]' | tr -d '-'
- ;;
- "namespace" | "class_name")
- echo "${input_string}" | tr '-' ' ' | tr '_' ' ' | awk -F" " '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));} 1' | tr -d ' -'
- ;;
- "package_name")
- echo "${input_string}" | tr ' ' '-' | tr '[:upper:]' '[:lower:]'
- ;;
- "function_name" | "ui_id" | "cli_command")
- echo "${input_string}" | tr ' ' '_' | tr '[:upper:]' '[:lower:]'
- ;;
- "log_entry" | "code_comment_title")
- echo "${input_string}"
- ;;
- *)
- echo "Invalid conversion type"
- ;;
- esac
-}
-
-replace_string_content() {
- local needle="${1}"
- local replacement="${2}"
- local sed_opts
- sed_opts=(-i) && [ "$(uname)" = "Darwin" ] && sed_opts=(-i '')
- set +e
- grep -rI --exclude-dir=".git" --exclude-dir=".idea" --exclude-dir="vendor" --exclude-dir="node_modules" -l "${needle}" "$(pwd)" | xargs sed "${sed_opts[@]}" "s!$needle!$replacement!g" || true
- set -e
-}
-
-remove_string_content() {
- local token="${1}"
- local sed_opts
- sed_opts=(-i) && [ "$(uname)" == "Darwin" ] && sed_opts=(-i '')
- grep -rI --exclude-dir=".git" --exclude-dir=".idea" --exclude-dir="vendor" --exclude-dir="node_modules" -l "${token}" "$(pwd)" | LC_ALL=C.UTF-8 xargs sed "${sed_opts[@]}" -e "/^${token}/d" || true
-}
-
-remove_tokens_with_content() {
- local token="${1}"
- local sed_opts
- sed_opts=(-i) && [ "$(uname)" == "Darwin" ] && sed_opts=(-i '')
- grep -rI --include=".*" --include="*" --exclude-dir=".git" --exclude-dir=".idea" --exclude-dir="vendor" --exclude-dir="node_modules" -l "#;> $token" "$(pwd)" | LC_ALL=C.UTF-8 xargs sed "${sed_opts[@]}" -e "/#;< $token/,/#;> $token/d" || true
-}
-
-uncomment_line() {
- local file_name="${1}"
- local start_string="${2}"
- local sed_opts
- sed_opts=(-i) && [ "$(uname)" == "Darwin" ] && sed_opts=(-i '')
- LC_ALL=C.UTF-8 sed "${sed_opts[@]}" -e "s/^# ${start_string}/${start_string}/" "${file_name}"
-}
-
-remove_special_comments() {
- local token="#;"
- local sed_opts
- sed_opts=(-i) && [ "$(uname)" == "Darwin" ] && sed_opts=(-i '')
- grep -rI --exclude-dir=".git" --exclude-dir=".idea" --exclude-dir="vendor" --exclude-dir="node_modules" -l "${token}" "$(pwd)" | LC_ALL=C.UTF-8 xargs sed "${sed_opts[@]}" -e "/${token}/d" || true
-}
-
-ask() {
- local prompt="$1"
- local default="${2-}"
- local result=""
-
- if [[ -n $default ]]; then
- prompt="${prompt} [${default}]: "
- else
- prompt="${prompt}: "
- fi
-
- while [[ -z ${result} ]]; do
- read -p "${prompt}" result
- if [[ -n $default && -z ${result} ]]; then
- result="${default}"
- fi
- done
- echo "${result}"
-}
-
-ask_yesno() {
- local prompt="${1}"
- local default="${2:-Y}"
- local result
-
- read -p "${prompt} [$([ "${default}" = "Y" ] && echo "Y/n" || echo "y/N")]: " result
- result="$(echo "${result:-${default}}" | tr '[:upper:]' '[:lower:]')"
- echo "${result}"
-}
-
-#-------------------------------------------------------------------------------
-
-remove_ci_provider_github_actions() {
- rm -rf .github/workflows >/dev/null 2>&1 || true
-}
-
-remove_ci_provider_circleci() {
- rm -rf .circleci >/dev/null 2>&1 || true
-}
-
-remove_command_wrapper_ahoy() {
- rm -rf .ahoy.yml >/dev/null 2>&1 || true
-}
-
-remove_command_wrapper_makefile() {
- rm -rf Makefile >/dev/null 2>&1 || true
-}
-
-process_readme() {
- mv README.dist.md "README.md" >/dev/null 2>&1 || true
-
- curl "https://placehold.jp/000000/ffffff/200x200.png?text=${1// /+}&css=%7B%22border-radius%22%3A%22%20100px%22%7D" >logo.tmp.png || true
- if [ -s "logo.tmp.png" ]; then
- mv logo.tmp.png "logo.png" >/dev/null 2>&1 || true
- fi
- rm logo.tmp.png >/dev/null 2>&1 || true
-}
-
-process_internal() {
- local extension_name="${1}"
- local extension_machine_name="${2}"
- local extension_type="${3}"
-
- extension_machine_name_class="$(convert_string "${extension_machine_name}" "class_name")"
-
- replace_string_content "YourNamespace" "${extension_machine_name}"
- replace_string_content "yournamespace" "${extension_machine_name}"
- replace_string_content "AlexSkrypnyk" "${extension_machine_name}"
- replace_string_content "alexskrypnyk" "${extension_machine_name}"
- replace_string_content "yourproject" "${extension_machine_name}"
- replace_string_content "Yourproject logo" "${extension_name} logo"
- replace_string_content "Your Extension" "${extension_name}"
- replace_string_content "your extension" "${extension_name}"
- replace_string_content "Your+Extension" "${extension_machine_name}"
- replace_string_content "your_extension" "${extension_machine_name}"
- replace_string_content "YourExtension" "${extension_machine_name_class}"
- replace_string_content "Provides your_extension functionality." "Provides ${extension_machine_name} functionality."
- replace_string_content "drupal-module" "drupal-${extension_type}"
- replace_string_content "Drupal module scaffold FE example used for template testing" "Provides ${extension_machine_name} functionality."
- replace_string_content "Drupal extension scaffold" "${extension_name}"
- replace_string_content "drupal_extension_scaffold" "${extension_machine_name}"
- replace_string_content "type: module" "type: ${extension_type}"
- replace_string_content "\[EXTENSION_NAME\]" "${extension_machine_name}"
-
- remove_string_content "# Uncomment the lines below in your project."
- uncomment_line ".gitattributes" ".ahoy.yml"
- uncomment_line ".gitattributes" ".circleci"
- uncomment_line ".gitattributes" ".devtools"
- uncomment_line ".gitattributes" ".editorconfig"
- uncomment_line ".gitattributes" ".gitattributes"
- uncomment_line ".gitattributes" ".github"
- uncomment_line ".gitattributes" ".gitignore"
- uncomment_line ".gitattributes" ".twig-cs-fixer.php"
- uncomment_line ".gitattributes" "Makefile"
- uncomment_line ".gitattributes" "composer.dev.json"
- uncomment_line ".gitattributes" "phpcs.xml"
- uncomment_line ".gitattributes" "phpmd.xml"
- uncomment_line ".gitattributes" "phpstan.neon"
- uncomment_line ".gitattributes" "rector.php"
- uncomment_line ".gitattributes" "renovate.json"
- uncomment_line ".gitattributes" "tests"
- remove_string_content "# Remove the lines below in your project."
- remove_string_content ".github\/FUNDING.yml export-ignore"
- remove_string_content "LICENSE export-ignore"
-
- mv "your_extension.info.yml" "${extension_machine_name}.info.yml"
- mv "your_extension.install" "${extension_machine_name}.install"
- mv "your_extension.links.menu.yml" "${extension_machine_name}.links.menu.yml"
- mv "your_extension.module" "${extension_machine_name}.module"
- mv "your_extension.routing.yml" "${extension_machine_name}.routing.yml"
- mv "your_extension.services.yml" "${extension_machine_name}.services.yml"
- mv "config/schema/your_extension.schema.yml" "config/schema/${extension_machine_name}.schema.yml"
- mv "src/Form/YourExtensionForm.php" "src/Form/${extension_machine_name_class}Form.php"
- mv "src/YourExtensionService.php" "src/${extension_machine_name_class}Service.php"
- mv "tests/src/Unit/YourExtensionServiceUnitTest.php" "tests/src/Unit/${extension_machine_name_class}ServiceUnitTest.php"
- mv "tests/src/Kernel/YourExtensionServiceKernelTest.php" "tests/src/Kernel/${extension_machine_name_class}ServiceKernelTest.php"
- mv "tests/src/Functional/YourExtensionFunctionalTest.php" "tests/src/Functional/${extension_machine_name_class}FunctionalTest.php"
-
- rm -f LICENSE >/dev/null || true
- rm -Rf "tests/scaffold" >/dev/null || true
- rm -f .github/workflows/scaffold*.yml >/dev/null || true
- rm -Rf .scaffold >/dev/null || true
-
- remove_tokens_with_content "META"
- remove_special_comments
-
- if [ "${extension_type}" = "theme" ]; then
- rm -rf tests >/dev/null || true
- echo 'base theme: false' >>"${extension_machine_name}.info.yml"
- fi
-}
-
-#-------------------------------------------------------------------------------
-
-main() {
- echo "Please follow the prompts to adjust your extension configuration"
- echo
-
- [ -z "${extension_name}" ] && extension_name="$(ask "Name")"
- extension_machine_name_default="$(convert_string "${extension_name}" "file_name")"
- [ -z "${extension_machine_name}" ] && extension_machine_name="$(ask "Machine name" "${extension_machine_name_default}")"
- extension_type_default="module"
- [ -z "${extension_type}" ] && extension_type="$(ask "Type: module or theme" "${extension_type_default}")"
- ci_provider_default="gha"
- [ -z "${ci_provider}" ] && ci_provider="$(ask "CI Provider: GitHub Actions (gha) or CircleCI (circleci)" "${ci_provider_default}")"
- command_wrapper_default="ahoy"
- [ -z "${command_wrapper}" ] && command_wrapper="$(ask "Command wrapper: Ahoy (ahoy), Makefile (makefile), None (none)" "${command_wrapper_default}")"
-
- remove_self="$(ask_yesno "Remove this script")"
-
- echo
- echo " Summary"
- echo "---------------------------------"
- echo "Name : ${extension_name}"
- echo "Machine name : ${extension_machine_name}"
- echo "Type : ${extension_type}"
- echo "CI Provider : ${ci_provider}"
- echo "Command wrapper : ${command_wrapper}"
- echo "Remove this script : ${remove_self}"
- echo "---------------------------------"
- echo
-
- should_proceed="$(ask_yesno "Proceed with project init")"
-
- if [ "${should_proceed}" != "y" ]; then
- echo
- echo "Aborting."
- exit 1
- fi
-
- #
- # Processing.
- #
-
- : "${extension_name:?name is required}"
- : "${extension_machine_name:?machine_name is required}"
- : "${extension_type:?type is required}"
- : "${ci_provider:?ci_provider is required}"
- : "${command_wrapper:?command_wrapper is required}"
-
- if [ "${ci_provider}" = "circleci" ]; then
- remove_ci_provider_github_actions
- else
- remove_ci_provider_circleci
- fi
-
- if [ "${command_wrapper}" = "ahoy" ]; then
- remove_command_wrapper_makefile
- elif [ "${command_wrapper}" = "makefile" ]; then
- remove_command_wrapper_ahoy
- else
- remove_command_wrapper_ahoy
- remove_command_wrapper_makefile
- fi
-
- process_readme "${extension_name}"
-
- process_internal "${extension_name}" "${extension_machine_name}" "${extension_type}" "${ci_provider}"
-
- [ "${remove_self}" != "n" ] && rm -- "$0" || true
-
- echo
- echo "Initialization complete."
-}
-
-if [ "$0" = "${BASH_SOURCE[0]}" ]; then
- main "$@"
-fi