diff --git a/.editorconfig b/.editorconfig index a7c44ddb..dd9a2b51 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,9 +2,9 @@ root = true [*] charset = utf-8 +end_of_line = lf indent_size = 4 indent_style = space -end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 3e1c4a2e..4937a98f 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -11,7 +11,7 @@ jobs: name: PHPStan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d512348a..80ecc324 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,18 +15,20 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.2] - laravel: [10.*] + php: [8.2, 8.3] + laravel: [10.*, 11.*] stability: [prefer-lowest, prefer-stable] include: - laravel: 10.* - testbench: 8.* + testbench: ^8.21 + - laravel: 11.* + testbench: 9.* name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index b20f3b6f..0cdea233 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: main @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: branch: main commit_message: Update CHANGELOG diff --git a/composer.json b/composer.json index 6dd0d7cc..0d4548e8 100644 --- a/composer.json +++ b/composer.json @@ -17,16 +17,16 @@ ], "require": { "php": "^8.2", - "spatie/laravel-package-tools": "^1.14.2", - "illuminate/contracts": "^10.0" + "spatie/laravel-package-tools": "^1.16", + "illuminate/contracts": "^10.0 || ^11.0" }, "require-dev": { - "nunomaduro/collision": "^7.0", - "nunomaduro/larastan": "^2.4.0", - "orchestra/testbench": "^8.0", - "pestphp/pest": "^2.0", - "pestphp/pest-plugin-laravel": "^2.0", - "worksome/coding-style": "^2.5" + "nunomaduro/collision": "^7.0 || ^8.0", + "larastan/larastan": "^2.6", + "orchestra/testbench": "^8.21 || ^9.0", + "pestphp/pest": "^2.33", + "pestphp/pest-plugin-laravel": "^2.2", + "worksome/coding-style": "^2.8" }, "autoload": { "psr-4": { diff --git a/configure.php b/configure.php index c6a714ea..aaaf0516 100644 --- a/configure.php +++ b/configure.php @@ -1,8 +1,9 @@ #!/usr/bin/env php $version) { + foreach ($data['require-dev'] as $name => $version) { if (in_array($name, $names, true)) { unset($data['require-dev'][$name]); } @@ -80,10 +95,11 @@ function remove_composer_deps(array $names) { file_put_contents(__DIR__.'/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } -function remove_composer_script($scriptName) { +function remove_composer_script($scriptName) +{ $data = json_decode(file_get_contents(__DIR__.'/composer.json'), true); - foreach($data['scripts'] as $name => $script) { + foreach ($data['scripts'] as $name => $script) { if ($scriptName === $name) { unset($data['scripts'][$name]); break; @@ -93,7 +109,8 @@ function remove_composer_script($scriptName) { file_put_contents(__DIR__.'/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } -function remove_readme_paragraphs(string $file): void { +function remove_readme_paragraphs(string $file): void +{ $contents = file_get_contents($file); file_put_contents( @@ -102,22 +119,126 @@ function remove_readme_paragraphs(string $file): void { ); } -function safeUnlink(string $filename) { +function safeUnlink(string $filename) +{ if (file_exists($filename) && is_file($filename)) { unlink($filename); } } -function determineSeparator(string $path): string { +function determineSeparator(string $path): string +{ return str_replace('/', DIRECTORY_SEPARATOR, $path); } -function replaceForWindows(): array { - return preg_split('/\\r\\n|\\r|\\n/', run('dir /S /B * | findstr /v /i .git\ | findstr /v /i vendor | findstr /v /i '.basename(__FILE__).' | findstr /r /i /M /F:/ ":author :vendor :package VendorName skeleton vendor_name vendor_slug author@domain.com"')); +function replaceForWindows(): array +{ + return preg_split('/\\r\\n|\\r|\\n/', run('dir /S /B * | findstr /v /i .git\ | findstr /v /i vendor | findstr /v /i '.basename(__FILE__).' | findstr /r /i /M /F:/ ":author :vendor :package VendorName skeleton migration_table_name vendor_name vendor_slug author@domain.com"')); } -function replaceForAllOtherOSes(): array { - return explode(PHP_EOL, run('grep -E -r -l -i ":author|:vendor|:package|VendorName|skeleton|vendor_name|vendor_slug|author@domain.com" --exclude-dir=vendor ./* ./.github/* | grep -v ' . basename(__FILE__))); +function replaceForAllOtherOSes(): array +{ + return explode(PHP_EOL, run('grep -E -r -l -i ":author|:vendor|:package|VendorName|skeleton|migration_table_name|vendor_name|vendor_slug|author@domain.com" --exclude-dir=vendor ./* ./.github/* | grep -v '.basename(__FILE__))); +} + +function getGitHubApiEndpoint(string $endpoint): ?stdClass +{ + try { + $curl = curl_init("https://api.github.com/{$endpoint}"); + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTPGET => true, + CURLOPT_HTTPHEADER => [ + 'User-Agent: spatie-configure-script/1.0', + ], + ]); + + $response = curl_exec($curl); + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + curl_close($curl); + + if ($statusCode === 200) { + return json_decode($response); + } + } catch (Exception $e) { + // ignore + } + + return null; +} + +function searchCommitsForGitHubUsername(): string +{ + $authorName = strtolower(trim(shell_exec('git config user.name'))); + + $committersRaw = shell_exec("git log --author='@users.noreply.github.com' --pretty='%an:%ae' --reverse"); + $committersLines = explode("\n", $committersRaw); + $committers = array_filter(array_map(function ($line) use ($authorName) { + $line = trim($line); + [$name, $email] = explode(':', $line) + [null, null]; + + return [ + 'name' => $name, + 'email' => $email, + 'isMatch' => strtolower($name) === $authorName && ! str_contains($name, '[bot]'), + ]; + }, $committersLines), fn ($item) => $item['isMatch']); + + if (empty($committers)) { + return ''; + } + + $firstCommitter = reset($committers); + + return explode('@', $firstCommitter['email'])[0] ?? ''; +} + +function guessGitHubUsernameUsingCli() +{ + try { + if (preg_match('/ogged in to github\.com as ([a-zA-Z-_]+).+/', shell_exec('gh auth status -h github.com 2>&1'), $matches)) { + return $matches[1]; + } + } catch (Exception $e) { + // ignore + } + + return ''; +} + +function guessGitHubUsername(): string +{ + $username = searchCommitsForGitHubUsername(); + if (! empty($username)) { + return $username; + } + + $username = guessGitHubUsernameUsingCli(); + if (! empty($username)) { + return $username; + } + + // fall back to using the username from the git remote + $remoteUrl = shell_exec('git config remote.origin.url'); + $remoteUrlParts = explode('/', str_replace(':', '/', trim($remoteUrl))); + + return $remoteUrlParts[1] ?? ''; +} + +function guessGitHubVendorInfo($authorName, $username): array +{ + $remoteUrl = shell_exec('git config remote.origin.url'); + $remoteUrlParts = explode('/', str_replace(':', '/', trim($remoteUrl))); + + $response = getGitHubApiEndpoint("orgs/{$remoteUrlParts[1]}"); + + if ($response === null) { + return [$authorName, $username]; + } + + return [$response->name ?? $authorName, $response->login ?? $username]; } $gitName = run('git config user.name'); @@ -125,15 +246,15 @@ function replaceForAllOtherOSes(): array { $gitEmail = run('git config user.email'); $authorEmail = ask('Author email', $gitEmail); +$authorUsername = ask('Author username', guessGitHubUsername()); + +$guessGitHubVendorInfo = guessGitHubVendorInfo($authorName, $authorUsername); -$usernameGuess = explode(':', run('git config remote.origin.url'))[1]; -$usernameGuess = dirname($usernameGuess); -$usernameGuess = basename($usernameGuess); -$authorUsername = ask('Author username', $usernameGuess); +$vendorName = ask('Vendor name', $guessGitHubVendorInfo[0]); +$vendorUsername = ask('Vendor username', $guessGitHubVendorInfo[1] ?? slugify($vendorName)); +$vendorSlug = slugify($vendorUsername); -$vendorName = ask('Vendor name', $authorUsername); -$vendorSlug = slugify($vendorName); -$vendorNamespace = ucwords($vendorName); +$vendorNamespace = str_replace('-', '', ucwords($vendorName)); $vendorNamespace = ask('Vendor namespace', $vendorNamespace); $currentDirectory = getcwd(); @@ -149,6 +270,9 @@ function replaceForAllOtherOSes(): array { $description = ask('Package description', "This is my package {$packageSlug}"); $usePhpStan = confirm('Enable PhpStan?', true); +$useLaravelPint = confirm('Enable Laravel Pint?', true); +$useDependabot = confirm('Enable Dependabot?', true); +$useLaravelRay = confirm('Use Ray for debugging?', true); $useUpdateChangelogWorkflow = confirm('Use automatic changelog updater workflow?', true); writeln('------'); @@ -157,10 +281,13 @@ function replaceForAllOtherOSes(): array { writeln("Package : {$packageSlug} <{$description}>"); writeln("Namespace : {$vendorNamespace}\\{$className}"); writeln("Class name : {$className}"); -writeln("---"); -writeln("Packages & Utilities"); -writeln("Use Larastan/PhpStan : " . ($usePhpStan ? 'yes' : 'no')); -writeln("Use Auto-Changelog : " . ($useUpdateChangelogWorkflow ? 'yes' : 'no')); +writeln('---'); +writeln('Packages & Utilities'); +writeln('Use Laravel/Pint : '.($useLaravelPint ? 'yes' : 'no')); +writeln('Use Larastan/PhpStan : '.($usePhpStan ? 'yes' : 'no')); +writeln('Use Dependabot : '.($useDependabot ? 'yes' : 'no')); +writeln('Use Ray App : '.($useLaravelRay ? 'yes' : 'no')); +writeln('Use Auto-Changelog : '.($useUpdateChangelogWorkflow ? 'yes' : 'no')); writeln('------'); writeln('This script will replace the above values in all relevant files in the project directory.'); @@ -179,48 +306,61 @@ function replaceForAllOtherOSes(): array { ':vendor_name' => $vendorName, ':vendor_slug' => $vendorSlug, 'VendorName' => $vendorNamespace, - 'vendor-slug' => $vendorSlug, ':package_name' => $packageName, ':package_slug' => $packageSlug, ':package_slug_without_prefix' => $packageSlugWithoutPrefix, - 'package-slug' => $packageSlug, 'Skeleton' => $className, 'skeleton' => $packageSlug, + 'migration_table_name' => title_snake($packageSlug), 'variable' => $variableName, ':package_description' => $description, ]); match (true) { - str_contains($file, determineSeparator('src/Skeleton.php')) => rename($file, determineSeparator('./src/' . $className . '.php')), - str_contains($file, determineSeparator('src/SkeletonServiceProvider.php')) => rename($file, determineSeparator('./src/' . $className . 'ServiceProvider.php')), - str_contains($file, determineSeparator('src/Facades/Skeleton.php')) => rename($file, determineSeparator('./src/Facades/' . $className . '.php')), - str_contains($file, determineSeparator('src/Commands/SkeletonCommand.php')) => rename($file, determineSeparator('./src/Commands/' . $className . 'Command.php')), - str_contains($file, determineSeparator('database/migrations/create_skeleton_table.php.stub')) => rename($file, determineSeparator('./database/migrations/create_' . $packageSlugWithoutPrefix . '_table.php.stub')), - str_contains($file, determineSeparator('config/skeleton.php')) => rename($file, determineSeparator('./config/' . $packageSlugWithoutPrefix . '.php')), + str_contains($file, determineSeparator('src/Skeleton.php')) => rename($file, determineSeparator('./src/'.$className.'.php')), + str_contains($file, determineSeparator('src/SkeletonServiceProvider.php')) => rename($file, determineSeparator('./src/'.$className.'ServiceProvider.php')), + str_contains($file, determineSeparator('src/Facades/Skeleton.php')) => rename($file, determineSeparator('./src/Facades/'.$className.'.php')), + str_contains($file, determineSeparator('src/Commands/SkeletonCommand.php')) => rename($file, determineSeparator('./src/Commands/'.$className.'Command.php')), + str_contains($file, determineSeparator('database/migrations/create_skeleton_table.php.stub')) => rename($file, determineSeparator('./database/migrations/create_'.title_snake($packageSlugWithoutPrefix).'_table.php.stub')), + str_contains($file, determineSeparator('config/skeleton.php')) => rename($file, determineSeparator('./config/'.$packageSlugWithoutPrefix.'.php')), str_contains($file, 'README.md') => remove_readme_paragraphs($file), default => [], }; } +if (! $useLaravelPint) { + safeUnlink(__DIR__.'/.github/workflows/fix-php-code-style-issues.yml'); + safeUnlink(__DIR__.'/pint.json'); +} + if (! $usePhpStan) { - safeUnlink(__DIR__ . '/phpstan.neon'); - safeUnlink(__DIR__ . '/phpstan-baseline.neon'); - safeUnlink(__DIR__ . '/.github/workflows/phpstan.yml'); + safeUnlink(__DIR__.'/phpstan.neon.dist'); + safeUnlink(__DIR__.'/phpstan-baseline.neon'); + safeUnlink(__DIR__.'/.github/workflows/phpstan.yml'); remove_composer_deps([ 'phpstan/extension-installer', 'phpstan/phpstan-deprecation-rules', 'phpstan/phpstan-phpunit', - 'nunomaduro/larastan', + 'larastan/larastan', ]); remove_composer_script('phpstan'); } +if (! $useDependabot) { + safeUnlink(__DIR__.'/.github/dependabot.yml'); + safeUnlink(__DIR__.'/.github/workflows/dependabot-auto-merge.yml'); +} + +if (! $useLaravelRay) { + remove_composer_deps(['spatie/laravel-ray']); +} + if (! $useUpdateChangelogWorkflow) { - safeUnlink(__DIR__ . '/.github/workflows/update-changelog.yml'); + safeUnlink(__DIR__.'/.github/workflows/update-changelog.yml'); } -confirm('Execute `composer install` and run tests?') && run('composer install && composer lint && composer test'); +confirm('Execute `composer install` and run tests?') && run('composer install && composer test'); confirm('Let this script delete itself?', true) && unlink(__FILE__); diff --git a/database/migrations/create_skeleton_table.php.stub b/database/migrations/create_skeleton_table.php.stub index 0e4bef73..2efdce96 100644 --- a/database/migrations/create_skeleton_table.php.stub +++ b/database/migrations/create_skeleton_table.php.stub @@ -8,7 +8,7 @@ return new class extends Migration { public function up() { - Schema::create('skeleton_table', function (Blueprint $table) { + Schema::create('migration_table_name_table', function (Blueprint $table) { $table->id(); // add fields diff --git a/src/Facades/Skeleton.php b/src/Facades/Skeleton.php index 5a3d817a..ea88dd94 100644 --- a/src/Facades/Skeleton.php +++ b/src/Facades/Skeleton.php @@ -13,6 +13,6 @@ class Skeleton extends Facade { protected static function getFacadeAccessor() { - return 'skeleton'; + return \VendorName\Skeleton\Skeleton::class; } } diff --git a/tests/ArchTest.php b/tests/ArchTest.php new file mode 100644 index 00000000..0160ae22 --- /dev/null +++ b/tests/ArchTest.php @@ -0,0 +1,7 @@ +expect(['dd', 'dump', 'ray']) + ->each->not->toBeUsed();