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