diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4255f35 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +* text=auto + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +/tests export-ignore +/tests-output export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.styleci.yml export-ignore +CHANGELOG.md export-ignore +phpstan.neon.dist export-ignore +phpunit.xml.dist export-ignore +RELEASE.md export-ignore diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..92b5bf5 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +The Laravel Code of Conduct can be found in the [Laravel documentation](https://laravel.com/docs/contributions#code-of-conduct). diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..38ca9f8 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contribution Guide + +The Laravel contributing guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml new file mode 100644 index 0000000..bc7871f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -0,0 +1,26 @@ +name: Bug Report +description: "Report something that's broken." +body: + - type: markdown + attributes: + value: "Please read [our full contribution guide](https://laravel.com/docs/contributions#bug-reports) before submitting bug reports. If you notice improper DocBlock, PHPStan, or IDE warnings while using Laravel, do not create a GitHub issue. Instead, please submit a pull request to fix the problem." + - type: input + attributes: + label: Installer Version + description: Provide the Installer version that you are using. + placeholder: 4.1.0 + validations: + required: true + - type: textarea + attributes: + label: Description + description: Provide a detailed description of the issue you are facing. + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Provide detailed steps to reproduce your issue. If necessary, please provide a GitHub repository to demonstrate your issue using `laravel new bug-report --github="--public"`. + validations: + required: true + diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 0000000..e530d76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,4 @@ +--- +name: "Feature request" +about: 'For ideas or feature requests, please make a pull request, or open an issue' +--- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6253bb2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support Questions & Other + url: https://laravel.com/docs/contributions#support-questions + about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:' + - name: Documentation issue + url: https://github.com/laravel/docs + about: For documentation issues, open a pull request at the laravel/docs repository diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0378693 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..dd673d4 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,92 @@ +# Security Policy + +**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).** + +## Supported Versions + +Only the latest major version receives security fixes. + +## Reporting a Vulnerability + +If you discover a security vulnerability within Laravel, please send an email to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed. + +### Public PGP Key + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP v2.0.8 +Comment: https://sela.io/pgp/ + +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 +WBGOG/qJGDlNiqBYYt2xNqzHCJoC +=zXOv +-----END PGP PUBLIC KEY BLOCK----- +``` diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..f0877fc --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +# Support Questions + +The Laravel support guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions#support-questions). diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 0000000..9634a0e --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,12 @@ +name: issues + +on: + issues: + types: [labeled] + +permissions: + issues: write + +jobs: + help-wanted: + uses: laravel/.github/.github/workflows/issues.yml@main diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml new file mode 100644 index 0000000..18b32b3 --- /dev/null +++ b/.github/workflows/pull-requests.yml @@ -0,0 +1,12 @@ +name: pull requests + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + +jobs: + uneditable: + uses: laravel/.github/.github/workflows/pull-requests.yml@main diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..368c185 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,15 @@ +name: static analysis + +on: + push: + branches: + - master + - '*.x' + pull_request: + +permissions: + contents: read + +jobs: + tests: + uses: laravel/.github/.github/workflows/static-analysis.yml@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b204a66 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,79 @@ +name: tests + +on: + push: + branches: + - master + - '*.x' + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + linux_tests: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + matrix: + php: [8.2, 8.3] + laravel: [10, 11] + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, fileinfo + tools: composer:v2 + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework=^${{ matrix.laravel }}" --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit + + windows_tests: + runs-on: windows-latest + + strategy: + fail-fast: true + matrix: + php: [8.2, 8.3] + laravel: [10, 11] + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Windows + + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, fileinfo + ini-values: error_reporting=E_ALL, memory_limit=512M + tools: composer:v2 + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework=~${{ matrix.laravel }}" --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 0000000..1625bda --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,9 @@ +name: update changelog + +on: + release: + types: [released] + +jobs: + update: + uses: laravel/.github/.github/workflows/update-changelog.yml@main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a932082 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.lock +phpunit.xml +.phpunit.result.cache diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..215fbcf --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +php: + preset: laravel +js: true +css: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6183035 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,368 @@ +# Release Notes + +## [Unreleased](https://github.com/laravel/installer/compare/v5.4.0...master) + +## [v5.4.0](https://github.com/laravel/installer/compare/v5.3.0...v5.4.0) - 2024-01-23 + +* SQLite by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/installer/pull/304 +* [5.x] Implies only the new migrations behaviour on L11 by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/305 +* [5.x] Improves ending message by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/307 + +## [v5.3.0](https://github.com/laravel/installer/compare/v5.2.1...v5.3.0) - 2024-01-16 + +* [5.x] Laravel v11 support by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/302 + +## [v5.2.1](https://github.com/laravel/installer/compare/v5.2.0...v5.2.1) - 2024-01-09 + +* Adjust link to docs by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/installer/pull/299 +* [5.x] Added default value on selection question while creating new application by [@bigship-prashant](https://github.com/bigship-prashant) in https://github.com/laravel/installer/pull/300 + +## [v5.2.0](https://github.com/laravel/installer/compare/v5.1.3...v5.2.0) - 2023-12-05 + +* Apply using the str_starts_with function by [@peter279k](https://github.com/peter279k) in https://github.com/laravel/installer/pull/289 +* Add mariadb installation option by [@Jubeki](https://github.com/Jubeki) in https://github.com/laravel/installer/pull/292 +* [5.x] Removes alias by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/293 + +## [v5.1.3](https://github.com/laravel/installer/compare/v5.1.2...v5.1.3) - 2023-10-10 + +- Remove extra DB_DATABASE by [@ConnySjoblom](https://github.com/ConnySjoblom) in https://github.com/laravel/installer/pull/287 +- Adjusts new command with new Breeze options by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/288 + +## [v5.1.2](https://github.com/laravel/installer/compare/v5.1.1...v5.1.2) - 2023-09-26 + +- Add the Livewire stack by [@mpociot](https://github.com/mpociot) in https://github.com/laravel/installer/pull/285 + +## [v5.1.1](https://github.com/laravel/installer/compare/v5.1.0...v5.1.1) - 2023-09-12 + +- Fix Jetstream SSR option by [@jessarcher](https://github.com/jessarcher) in https://github.com/laravel/installer/pull/281 + +## [v5.1.0](https://github.com/laravel/installer/compare/v5.0.4...v5.1.0) - 2023-08-29 + +- Fixes breeze installation with `--ssr` options by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/installer/pull/278 +- Adds missing jetstream options by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/279 +- Add ability to select default database connection by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/installer/pull/275 +- Updated to use the Process constructor to install Pest and use `Composer` helper class by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/installer/pull/274 + +## [v5.0.4](https://github.com/laravel/installer/compare/v5.0.3...v5.0.4) - 2023-08-22 + +No major changes. + +## [v5.0.3](https://github.com/laravel/installer/compare/v5.0.2...v5.0.3) - 2023-08-15 + +- Add option to indicate whether Breeze should be scaffolded with TypeScript support by [@weavdale](https://github.com/weavdale) in https://github.com/laravel/installer/pull/271 + +## [v5.0.2](https://github.com/laravel/installer/compare/v5.0.1...v5.0.2) - 2023-08-08 + +- Show the directory name at the end of the installer by [@jessarcher](https://github.com/jessarcher) in https://github.com/laravel/installer/pull/270 + +## [v5.0.1](https://github.com/laravel/installer/compare/v5.0.0...v5.0.1) - 2023-08-03 + +- Don't prompt for Git if Github Option is found by [@Jubeki](https://github.com/Jubeki) in https://github.com/laravel/installer/pull/269 + +## [v5.0.0](https://github.com/laravel/installer/compare/v4.5.1...v5.0.0) - 2023-08-01 + +- Update prompts by [@jessarcher](https://github.com/jessarcher) in https://github.com/laravel/installer/pull/267 + +## [v4.5.1](https://github.com/laravel/installer/compare/v4.5.0...v4.5.1) - 2023-07-25 + +- Wrap PHP_BINARY in quotes by [@mpociot](https://github.com/mpociot) in https://github.com/laravel/installer/pull/268 + +## [v4.5.0](https://github.com/laravel/installer/compare/v4.4.3...v4.5.0) - 2023-03-20 + +- Use Pest v2 by @nunomaduro in https://github.com/laravel/installer/pull/262 + +## [v4.4.3](https://github.com/laravel/installer/compare/v4.4.2...v4.4.3) - 2023-03-07 + +- Prevent unknown option with git commands by @zepfietje in https://github.com/laravel/installer/pull/260 + +## [v4.4.2](https://github.com/laravel/installer/compare/v4.4.1...v4.4.2) - 2023-03-02 + +- Adds option "dark" to jetstream installation by @MarioPerini in https://github.com/laravel/installer/pull/259 + +## [v4.4.1](https://github.com/laravel/installer/compare/v4.4.0...v4.4.1) - 2023-02-21 + +- Add an option to install PHPUnit by @likeadeckofcards in https://github.com/laravel/installer/pull/256 + +## [v4.4.0](https://github.com/laravel/installer/compare/v4.3.0...v4.4.0) - 2023-02-14 + +### Added + +- Adds Laravel Breeze scaffolding by @nunomaduro in https://github.com/laravel/installer/pull/253 + +### Fixed + +- Fixes installation of Pest in Laravel 10 by @nunomaduro in https://github.com/laravel/installer/pull/254 + +## [v4.3.0](https://github.com/laravel/installer/compare/v4.2.17...v4.3.0) - 2023-02-07 + +### Added + +- Adds `--pest` flag to use Pest by @nunomaduro in https://github.com/laravel/installer/pull/251 + +## [v4.2.17](https://github.com/laravel/installer/compare/v4.2.16...v4.2.17) - 2022-09-13 + +### Changed + +- Remove `storage:link` command by @jessarcher in https://github.com/laravel/installer/pull/245 + +## [v4.2.16](https://github.com/laravel/installer/compare/v4.2.15...v4.2.16) - 2022-08-16 + +### Changed + +- Remove duplicate `npm install` and `npm run build` steps by @jessarcher in https://github.com/laravel/installer/pull/242 + +## [v4.2.15](https://github.com/laravel/installer/compare/v4.2.14...v4.2.15) - 2022-08-09 + +### Fixed + +- Changes APP_URL replacement with name to lower by @RhysLees in https://github.com/laravel/installer/pull/239 +- Prevent unresolvable `APP_URL` by @jessarcher in https://github.com/laravel/installer/pull/240 + +## [v4.2.14](https://github.com/laravel/installer/compare/v4.2.13...v4.2.14) - 2022-08-02 + +### Changed + +- Simplify push to GitHub by @driesvints in https://github.com/laravel/installer/pull/238 + +## [v4.2.13](https://github.com/laravel/installer/compare/v4.2.12...v4.2.13) - 2022-07-26 + +### Changed + +- Improved console output by @nunomaduro in https://github.com/laravel/installer/pull/235 + +## [v4.2.12](https://github.com/laravel/installer/compare/v4.2.11...v4.2.12) - 2022-07-13 + +### Fixed + +- Check directory before deleting in Windows OS by @azizramdan in https://github.com/laravel/installer/pull/233 + +## [v4.2.11](https://github.com/laravel/installer/compare/v4.2.10...v4.2.11) - 2022-06-28 + +### Fixed + +- Use build command instead of dev command for Vite support by @driesvints in https://github.com/laravel/installer/pull/232 + +## [v4.2.10 (2022-01-18)](https://github.com/laravel/installer/compare/v4.2.9...v4.2.10) + +### Changed + +- Symfony v6 support ([#217](https://github.com/laravel/installer/pull/217)) + +### Fixed + +- Maintain current functionality with gh repo create rewrite ([#219](https://github.com/laravel/installer/pull/219)) + +## [v4.2.9 (2021-10-26)](https://github.com/laravel/installer/compare/v4.2.8...v4.2.9) + +### Changed + +- Always run NPM install ([#214](https://github.com/laravel/installer/pull/214)) + +## [v4.2.8 (2021-08-17)](https://github.com/laravel/installer/compare/v4.2.7...v4.2.8) + +### Changed + +- Respect git global config for a default branch ([#207](https://github.com/laravel/installer/pull/207)) + +## [v4.2.7 (2021-06-08)](https://github.com/laravel/installer/compare/v4.2.6...v4.2.7) + +### Fixed + +- Use `isDecorated` for no-ansi detection ([#203](https://github.com/laravel/installer/pull/203)) + +## [v4.2.6 (2021-06-01)](https://github.com/laravel/installer/compare/v4.2.5...v4.2.6) + +### Fixed + +- Fix new command failing without `no-ansi` option ([#202](https://github.com/laravel/installer/pull/202)) + +## [v4.2.5 (2021-04-27)](https://github.com/laravel/installer/compare/v4.2.4...v4.2.5) + +### Changed + +- Add support for older versions of Git ([#199](https://github.com/laravel/installer/pull/199)) + +## [v4.2.4 (2021-03-23)](https://github.com/laravel/installer/compare/v4.2.3...v4.2.4) + +### Added + +- Add branch flag ([#197](https://github.com/laravel/installer/pull/197), [a8d5c2d](https://github.com/laravel/installer/commit/a8d5c2d2ff7df892b567ffea19527b0c4451b750)) + +### Changed + +- Update pushing branch ([#196](https://github.com/laravel/installer/pull/196)) + +## [v4.2.3 (2021-03-18)](https://github.com/laravel/installer/compare/v4.2.2...v4.2.3) + +### Added + +- Add organization flag ([#194](https://github.com/laravel/installer/pull/194)) + +## [v4.2.2 (2021-03-16)](https://github.com/laravel/installer/compare/v4.2.1...v4.2.2) + +### Changed + +- Revert "Improve Git push" ([#192](https://github.com/laravel/installer/pull/192)) + +## [v4.2.1 (2021-03-16)](https://github.com/laravel/installer/compare/v4.2.0...v4.2.1) + +### Changed + +- Improve Git push ([#191](https://github.com/laravel/installer/pull/191)) + +## [v4.2.0 (2021-03-09)](https://github.com/laravel/installer/compare/v4.1.1...v4.2.0) + +### Added + +- Git Support ([#185](https://github.com/laravel/installer/pull/185)) + +## [v4.1.1 (2020-11-17)](https://github.com/laravel/installer/compare/v4.1.0...v4.1.1) + +### Changed + +- Require name argument ([#178](https://github.com/laravel/installer/pull/178)) + +## [v4.1.0 (2020-11-03)](https://github.com/laravel/installer/compare/v4.0.7...v4.1.0) + +### Added + +- PHP 8 Support ([#168](https://github.com/laravel/installer/pull/168)) + +### Changed + +- Use `dev-master` for `dev` version ([9ce64f82](https://github.com/laravel/installer/commit/9ce64f82dcc6d700d91e34b7bcfc32f0b16e2839)) + +## [v4.0.7 (2020-10-30)](https://github.com/laravel/installer/compare/v4.0.6...v4.0.7) + +### Fixed + +- Fixed some jetstream prompt issues + +## [v4.0.6 (2020-10-30)](https://github.com/laravel/installer/compare/v4.0.5...v4.0.6) + +### Added + +- Add prompt-jetstream switch ([95c3a00](https://github.com/laravel/installer/commit/95c3a00ee7fc188121ae3e90292f712eae19b26b)) + +### Changed + +- Update `DB_DATABASE` in `.env.example` ([#167](https://github.com/laravel/installer/pull/167)) + +## [v4.0.5 (2020-09-22)](https://github.com/laravel/installer/compare/v4.0.4...v4.0.5) + +### Fixed + +- Ensure artisan command is executable ([#153](https://github.com/laravel/installer/pull/153)) +- Fix quiet and no-ansi flags ([#156](https://github.com/laravel/installer/pull/156)) + +## [v4.0.4 (2020-09-15)](https://github.com/laravel/installer/compare/v4.0.3...v4.0.4) + +### Fixed + +- Close `` tag ([#149](https://github.com/laravel/installer/pull/149)) +- Add warning about `--force` and installing in current directory ([#152](https://github.com/laravel/installer/pull/152)) + +## [v4.0.3 (2020-09-08)](https://github.com/laravel/installer/compare/v4.0.2...v4.0.3) + +### Fixed + +- Fix for directories with spaces in current working directory path ([#147](https://github.com/laravel/installer/pull/147)) + +## [v4.0.2 (2020-09-08)](https://github.com/laravel/installer/compare/v4.0.1...v4.0.2) + +### Added + +- Add stack and teams options ([#143](https://github.com/laravel/installer/pull/143)) + +## [v4.0.1 (2020-09-07)](https://github.com/laravel/installer/compare/v4.0.0...v4.0.1) + +### Changed + +- Require PHP 7.3 ([#132](https://github.com/laravel/installer/pull/132)) + +### Fixed + +- Fix multiple issues when running on Windows ([#133](https://github.com/laravel/installer/pull/133), [#137](https://github.com/laravel/installer/pull/137)) +- Only change `.env` file when project name exists ([#140](https://github.com/laravel/installer/pull/140)) + +## [v4.0.0 (2020-09-03)](https://github.com/laravel/installer/compare/v3.2.0...v4.0.0) + +### Changed + +- Switch to `composer create-project` ([#124](https://github.com/laravel/installer/pull/124), [562650d](https://github.com/laravel/installer/commit/562650de8b637253b7ae47c3383bdd20e8419d1c), [8ab3502](https://github.com/laravel/installer/commit/8ab3502f1d5561d10cf1767213ec0c008baa145b)) + +## [v3.2.0 (2020-06-30)](https://github.com/laravel/installer/compare/v3.1.0...v3.2.0) + +### Added + +- Guzzle 7 support ([144a695](https://github.com/laravel/installer/commit/144a69576bfb0df2bbd5c7ae3f40dd87db64d0ba)) + +## [v3.1.0 (2020-05-21)](https://github.com/laravel/installer/compare/v3.0.1...v3.1.0) + +### Removed + +- Drop support for PHP 7.2 ([#118](https://github.com/laravel/installer/pull/118)) + +## [v3.0.1 (2019-11-26)](https://github.com/laravel/installer/compare/v3.0.0...v3.0.1) + +### Fixed + +- Fix composer autoloader path ([f3db3f3](https://github.com/laravel/installer/commit/f3db3f306c3c2dbbf4ecce4a5dbefe6c1fd178be)) + +## [v3.0.0 (2019-11-26)](https://github.com/laravel/installer/compare/v2.3.0...v3.0.0) + +### Changed + +- Move `laravel` binary to new directory ([c581a78](https://github.com/laravel/installer/commit/c581a784643911b97c3b8a2ec25ac809eadbf9c5)) +- Require PHP 7.2 as the new minimum version ([3ab97f2](https://github.com/laravel/installer/commit/3ab97f2e454d9c95833ccdd141d2fdbcdc8e0066)) +- Allow Symfony 5 ([513a060](https://github.com/laravel/installer/commit/513a060e9877bc8ab222d7ff4a60bc97131a0a0c)) + +### Removed + +- Remove Symfony 3.x support ([a09d8fe](https://github.com/laravel/installer/commit/a09d8fe2ced9579d4fce445aa1336b0993e3e9d0)) +- Remove `zipper.sh` ([78ef1db](https://github.com/laravel/installer/commit/78ef1dbe9ad2fbe5f16a85917748f89bb372599f)) + +## [v2.3.0 (2019-11-19)](https://github.com/laravel/installer/compare/v2.2.1...v2.3.0) + +### Added + +- Add `--auth` flag ([f5ebbff](https://github.com/laravel/installer/commit/f5ebbff32f9ff9c40fdf4c200cb2f396050e3cf3)) + +## [v2.2.1 (2019-10-29)](https://github.com/laravel/installer/compare/v2.2.0...v2.2.1) + +### Fixed + +- Make sure zip file is valid before extracting ([#100](https://github.com/laravel/installer/pull/100)) + +## [v2.2.0 (2019-10-15)](https://github.com/laravel/installer/compare/v2.1.0...v2.2.0) + +### Added + +- Create a new project in the current directory using "laravel new ." ([#99](https://github.com/laravel/installer/pull/99)) + +## [v2.1.0 (2019-04-30)](https://github.com/laravel/installer/compare/v2.0.1...v2.1.0) + +### Added + +- Added an alias to the `--force` option ([#79](https://github.com/laravel/installer/pull/79)) + +### Changed + +- Use the `extension_loaded` method to check if the 'zip' extension is loaded ([#81](https://github.com/laravel/installer/pull/81)) + +### Fixed + +- Respect `--quiet` option ([#77](https://github.com/laravel/installer/pull/77)) +- Update composer path on `findComposer` ([#86](https://github.com/laravel/installer/pull/86)) + +## [v2.0.1 (2018-02-01)](https://github.com/laravel/installer/compare/v2.0.0...v2.0.1) + +### Changed + +- Update dependencies ([6e34188](https://github.com/laravel/installer/commit/6e341883b9ba45be6a06f40c8e2c1b5033029d99)) + +## [v2.0.0 (2018-02-01)](https://github.com/laravel/installer/compare/v1.5.0...v2.0.0) + +### Changed + +- Bump guzzle requirement ([f909b98](https://github.com/laravel/installer/commit/f909b983e1b57f13b5b102f4c0c0fc1883fcbe22)) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..79810c8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Taylor Otwell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6b5b89 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Laravel Installer + +Build Status +Total Downloads +Latest Stable Version +License + +## Official Documentation + +Documentation for installing Laravel can be found on the [Laravel website](https://laravel.com/docs#creating-a-laravel-project). + +## Contributing + +Thank you for considering contributing to the Installer! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +Please review [our security policy](https://github.com/laravel/installer/security/policy) on how to report security vulnerabilities. + +## License + +Laravel Installer is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..d14d6ab --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,4 @@ +# Release Instructions + +1. Update the version in [`bin/laravel`](./bin/laravel) and commit it +2. [Create a new GitHub release](https://github.com/laravel/installer/releases/new) for this version with the release notes diff --git a/bin/filament b/bin/filament new file mode 100755 index 0000000..70cee45 --- /dev/null +++ b/bin/filament @@ -0,0 +1,13 @@ +#!/usr/bin/env php +add(new LaraZeus\Installer\NewCommand); + +$app->run(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6a7709d --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "lara-zeus/installer", + "description": "filament application installer.", + "keywords": ["filamentphp"], + "license": "MIT", + "authors": [ + { + "name": "php coder", + "email": "info@larazeus.com", + "role": "Owner" + } + ], + "require": { + "php": "^8.2", + "illuminate/filesystem": "^10.20|^11.0", + "illuminate/support": "^10.20|^11.0", + "laravel/prompts": "^0.1", + "symfony/console": "^6.2|^7.0", + "symfony/process": "^6.2|^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.4" + }, + "bin": [ + "bin/filament" + ], + "autoload": { + "psr-4": { + "LaraZeus\\Installer\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "LaraZeus\\Installer\\Tests\\": "tests/" + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..b52042e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + paths: + - bin + - src + + level: 0 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..02e7dfe --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,9 @@ + + + + + ./tests + ./tests/scaffolds + + + diff --git a/src/Concerns/ConfiguresPrompts.php b/src/Concerns/ConfiguresPrompts.php new file mode 100644 index 0000000..883b317 --- /dev/null +++ b/src/Concerns/ConfiguresPrompts.php @@ -0,0 +1,132 @@ +isInteractive() || PHP_OS_FAMILY === 'Windows'); + + TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid( + fn () => (new SymfonyStyle($input, $output))->ask($prompt->label, $prompt->default ?: null) ?? '', + $prompt->required, + $prompt->validate, + $output + )); + + PasswordPrompt::fallbackUsing(fn (PasswordPrompt $prompt) => $this->promptUntilValid( + fn () => (new SymfonyStyle($input, $output))->askHidden($prompt->label) ?? '', + $prompt->required, + $prompt->validate, + $output + )); + + ConfirmPrompt::fallbackUsing(fn (ConfirmPrompt $prompt) => $this->promptUntilValid( + fn () => (new SymfonyStyle($input, $output))->confirm($prompt->label, $prompt->default), + $prompt->required, + $prompt->validate, + $output + )); + + SelectPrompt::fallbackUsing(fn (SelectPrompt $prompt) => $this->promptUntilValid( + fn () => (new SymfonyStyle($input, $output))->choice($prompt->label, $prompt->options, $prompt->default), + false, + $prompt->validate, + $output + )); + + MultiSelectPrompt::fallbackUsing(function (MultiSelectPrompt $prompt) use ($input, $output) { + if ($prompt->default !== []) { + return $this->promptUntilValid( + fn () => (new SymfonyStyle($input, $output))->choice($prompt->label, $prompt->options, implode(',', $prompt->default), true), + $prompt->required, + $prompt->validate, + $output + ); + } + + return $this->promptUntilValid( + fn () => collect((new SymfonyStyle($input, $output))->choice( + $prompt->label, + array_is_list($prompt->options) + ? ['None', ...$prompt->options] + : ['none' => 'None', ...$prompt->options], + 'None', + true) + )->reject(array_is_list($prompt->options) ? 'None' : 'none')->all(), + $prompt->required, + $prompt->validate, + $output + ); + }); + + SuggestPrompt::fallbackUsing(fn (SuggestPrompt $prompt) => $this->promptUntilValid( + function () use ($prompt, $input, $output) { + $question = new Question($prompt->label, $prompt->default); + + is_callable($prompt->options) + ? $question->setAutocompleterCallback($prompt->options) + : $question->setAutocompleterValues($prompt->options); + + return (new SymfonyStyle($input, $output))->askQuestion($question); + }, + $prompt->required, + $prompt->validate, + $output + )); + } + + /** + * Prompt the user until the given validation callback passes. + * + * @param \Closure $prompt + * @param bool|string $required + * @param \Closure|null $validate + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return mixed + */ + protected function promptUntilValid($prompt, $required, $validate, $output) + { + while (true) { + $result = $prompt(); + + if ($required && ($result === '' || $result === [] || $result === false)) { + $output->writeln(''.(is_string($required) ? $required : 'Required.').''); + + continue; + } + + if ($validate) { + $error = $validate($result); + + if (is_string($error) && strlen($error) > 0) { + $output->writeln("{$error}"); + + continue; + } + } + + return $result; + } + } +} diff --git a/src/NewCommand.php b/src/NewCommand.php new file mode 100644 index 0000000..3603360 --- /dev/null +++ b/src/NewCommand.php @@ -0,0 +1,660 @@ +setName('new') + ->setDescription('Create a new Filament application') + ->addArgument('name', InputArgument::REQUIRED) + ->addArgument('panel', InputArgument::REQUIRED) + ->addOption('dev', null, InputOption::VALUE_NONE, 'Installs the latest "development" release') + ->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a Git repository') + ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The branch that should be created for a new repository', $this->defaultBranch()) + ->addOption('github', null, InputOption::VALUE_OPTIONAL, 'Create a new repository on GitHub', false) + ->addOption('organization', null, InputOption::VALUE_REQUIRED, 'The GitHub organization to create the new repository for') + ->addOption('pest', null, InputOption::VALUE_NONE, 'Installs the Pest testing framework') + ->addOption('phpunit', null, InputOption::VALUE_NONE, 'Installs the PHPUnit testing framework') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists'); + } + + /** + * Interact with the user before validating the input. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + parent::interact($input, $output); + + $this->configurePrompts($input, $output); + + $output->write(PHP_EOL.' + ███████╗██╗██╗ █████╗ ███╗ ███╗███████╗███╗ ██╗████████╗██████╗ ██╗ ██╗██████╗ + ██╔════╝██║██║ ██╔══██╗████╗ ████║██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██║ ██║██╔══██╗ + █████╗ ██║██║ ███████║██╔████╔██║█████╗ ██╔██╗ ██║ ██║ ██████╔╝███████║██████╔╝ + ██╔══╝ ██║██║ ██╔══██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║ ██║ ██╔═══╝ ██╔══██║██╔═══╝ + ██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗██║ ╚████║ ██║ ██║ ██║ ██║██║ + ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ + '.PHP_EOL.PHP_EOL); + + /*spin( + fn () => sleep(9), + 'Fetching response...' + );*/ + + if (! $input->getArgument('name')) { + $input->setArgument('name', text( + label: 'What is the name of your project?', + placeholder: 'E.g. example-app', + required: 'The project name is required.', + validate: fn ($value) => preg_match('/[^\pL\pN\-_.]/', $value) !== 0 + ? 'The name may only contain letters, numbers, dashes, underscores, and periods.' + : null, + )); + } + + if (! $input->getArgument('panel')) { + $input->setArgument('panel', text( + label: 'What is the name of your panel?', + placeholder: 'E.g. admin', + default: 'admin', + required: 'The panel name is required.', + validate: fn ($value) => preg_match('/[^\pL\pN\-_.]/', $value) !== 0 + ? 'The name may only contain letters, numbers, dashes, underscores, and periods.' + : null, + )); + } + + if (! $input->getOption('phpunit') && ! $input->getOption('pest')) { + $input->setOption('pest', select( + label: 'Which testing framework do you prefer?', + options: ['Pest', 'PHPUnit'], + default: 'Pest', + ) === 'Pest'); + } + + if (! $input->getOption('git') && $input->getOption('github') === false && Process::fromShellCommandline('git --version')->run() === 0) { + $input->setOption('git', confirm(label: 'Would you like to initialize a Git repository?', default: false)); + } + + if (! $input->getOption('migrate')) { + $input->setOption('migrate', confirm(label: 'Would you like to initialize a Git repository?', default: true)); + } + + if (! $input->getOption('database')) { + $input->setOption('database', select( + label: 'Which database will your application use?', + options: [ + 'mysql' => 'MySQL', + 'mariadb' => 'MariaDB', + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLite', + 'sqlsrv' => 'SQL Server', + ], + default: 'mysql', + ) === 'mysql'); + } + } + + /** + * Execute the command. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $name = $input->getArgument('name'); + + $directory = $name !== '.' ? getcwd().'/'.$name : '.'; + + $this->composer = new Composer(new Filesystem(), $directory); + + $version = $this->getVersion($input); + + if (! $input->getOption('force')) { + $this->verifyApplicationDoesntExist($directory); + } + + if ($input->getOption('force') && $directory === '.') { + throw new RuntimeException('Cannot use --force option when using current directory for installation!'); + } + + $composer = $this->findComposer(); + + $commands = [ + $composer." create-project laravel/laravel \"$directory\" $version --remove-vcs --prefer-dist", + ]; + + if ($directory != '.' && $input->getOption('force')) { + if (PHP_OS_FAMILY == 'Windows') { + array_unshift($commands, "(if exist \"$directory\" rd /s /q \"$directory\")"); + } else { + array_unshift($commands, "rm -rf \"$directory\""); + } + } + + if (PHP_OS_FAMILY != 'Windows') { + $commands[] = "chmod 755 \"$directory/artisan\""; + } + + if (($process = $this->runCommands($commands, $input, $output))->isSuccessful()) { + if ($name !== '.') { + $this->replaceInFile( + 'APP_URL=http://localhost', + 'APP_URL='.$this->generateAppUrl($name), + $directory.'/.env' + ); + + $database = $input->getOption('database'); + $migrate = $input->getOption('migrate'); + + $this->configureDefaultDatabaseConnection($directory, $database, $name, $migrate); + + if ($migrate) { + $this->runCommands([ + $this->phpBinary().' artisan migrate', + ], $input, $output, workingPath: $directory); + } + } + + if ($input->getOption('git') || $input->getOption('github') !== false) { + $this->createRepository($directory, $input, $output); + } + + if ($input->getOption('pest')) { + $this->installPest($directory, $input, $output); + } + + $this->installFilament($directory, $input, $output); + $output->writeln(''); + + $output->writeln(" INFO Application ready in [{$name}]. You can start your local development using:".PHP_EOL); + + $output->writeln('cd '.$name.''); + $output->writeln('php artisan serve'); + $output->writeln(''); + + $output->writeln(' New to Filament? Check out our bootcamp and documentation. Build something amazing!'); + $output->writeln(''); + } + + return $process->getExitCode(); + } + + /** + * Return the local machine's default Git branch if set or default to `main`. + * + * @return string + */ + protected function defaultBranch() + { + $process = new Process(['git', 'config', '--global', 'init.defaultBranch']); + + $process->run(); + + $output = trim($process->getOutput()); + + return $process->isSuccessful() && $output ? $output : 'main'; + } + + /** + * Configure the default database connection. + * + * @param string $directory + * @param string $database + * @param string $name + * @param bool $migrate + * @return void + */ + protected function configureDefaultDatabaseConnection(string $directory, string $database, string $name, bool $migrate) + { + // MariaDB configuration only exists as of Laravel 11... + if ($database === 'mariadb' && ! $this->usingLaravel11OrNewer($directory)) { + $database = 'mysql'; + } + + $this->pregReplaceInFile( + '/DB_CONNECTION=.*/', + 'DB_CONNECTION='.$database, + $directory.'/.env' + ); + + $this->pregReplaceInFile( + '/DB_CONNECTION=.*/', + 'DB_CONNECTION='.$database, + $directory.'/.env.example' + ); + + if ($database === 'sqlite') { + $environment = file_get_contents($directory.'/.env'); + + // If database options aren't commented, comment them for SQLite... + if (! str_contains($environment, '# DB_HOST=127.0.0.1')) { + $this->commentDatabaseConfigurationForSqlite($directory); + + return; + } + + return; + } + + // Any commented database configuration options should be uncommented when not on SQLite... + $this->uncommentDatabaseConfiguration($directory); + + $defaultPorts = [ + 'pgsql' => '5432', + 'sqlsrv' => '1433', + ]; + + if (isset($defaultPorts[$database])) { + $this->replaceInFile( + 'DB_PORT=3306', + 'DB_PORT='.$defaultPorts[$database], + $directory.'/.env' + ); + + $this->replaceInFile( + 'DB_PORT=3306', + 'DB_PORT='.$defaultPorts[$database], + $directory.'/.env.example' + ); + } + + $this->replaceInFile( + 'DB_DATABASE=filament', + 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), + $directory.'/.env' + ); + + $this->replaceInFile( + 'DB_DATABASE=filament', + 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), + $directory.'/.env.example' + ); + } + + /** + * Determine if the application is using Laravel 11 or newer. + * + * @param string $directory + * @return bool + */ + public function usingLaravel11OrNewer(string $directory): bool + { + $version = json_decode(file_get_contents($directory.'/composer.json'), true)['require']['laravel/framework']; + $version = str_replace('^', '', $version); + $version = explode('.', $version)[0]; + + return $version >= 11; + } + + /** + * Install FilamentPHP into the application. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installFilament(string $directory, InputInterface $input, OutputInterface $output) + { + $commands = array_filter([ + $this->findComposer().' require filament/filament', + $this->phpBinary().' artisan filament:install --panels', + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory); + + $this->commitChanges('Install Filament', $directory, $input, $output); + } + + /** + * Install Pest into the application. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installPest(string $directory, InputInterface $input, OutputInterface $output) + { + if ($this->removeComposerPackages(['phpunit/phpunit', '--no-update'], $output, true) + && $this->requireComposerPackages(['pestphp/pest:^2.0', 'pestphp/pest-plugin-laravel:^2.0'], $output, true)) { + $commands = array_filter([ + $this->phpBinary().' ./vendor/bin/pest --init', + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory, env: [ + 'PEST_NO_SUPPORT' => 'true', + ]); + + $this->replaceFile( + 'pest/Feature.php', + $directory.'/tests/Feature/ExampleTest.php', + ); + + $this->replaceFile( + 'pest/Unit.php', + $directory.'/tests/Unit/ExampleTest.php', + ); + + $this->commitChanges('Install Pest', $directory, $input, $output); + } + } + + /** + * Create a Git repository and commit the base Laravel skeleton. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function createRepository(string $directory, InputInterface $input, OutputInterface $output) + { + $branch = $input->getOption('branch') ?: $this->defaultBranch(); + + $commands = [ + 'git init -q', + 'git add .', + 'git commit -q -m "Set up a fresh Laravel app"', + "git branch -M {$branch}", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory); + } + + /** + * Commit any changes in the current working directory. + * + * @param string $message + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function commitChanges(string $message, string $directory, InputInterface $input, OutputInterface $output) + { + if (! $input->getOption('git') && $input->getOption('github') === false) { + return; + } + + $commands = [ + 'git add .', + "git commit -q -m \"$message\"", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory); + } + + /** + * Create a GitHub repository and push the git log to it. + * + * @param string $name + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function pushToGitHub(string $name, string $directory, InputInterface $input, OutputInterface $output) + { + $process = new Process(['gh', 'auth', 'status']); + $process->run(); + + if (! $process->isSuccessful()) { + $output->writeln(' WARN Make sure the "gh" CLI tool is installed and that you\'re authenticated to GitHub. Skipping...'.PHP_EOL); + + return; + } + + $name = $input->getOption('organization') ? $input->getOption('organization')."/$name" : $name; + $flags = $input->getOption('github') ?: '--private'; + + $commands = [ + "gh repo create {$name} --source=. --push {$flags}", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory, env: ['GIT_TERMINAL_PROMPT' => 0]); + } + + /** + * Verify that the application does not already exist. + * + * @param string $directory + * @return void + */ + protected function verifyApplicationDoesntExist($directory) + { + if ((is_dir($directory) || is_file($directory)) && $directory != getcwd()) { + throw new RuntimeException('Application already exists!'); + } + } + + /** + * Generate a valid APP_URL for the given application name. + * + * @param string $name + * @return string + */ + protected function generateAppUrl($name) + { + $hostname = mb_strtolower($name).'.test'; + + return $this->canResolveHostname($hostname) ? 'http://'.$hostname : 'http://localhost'; + } + + /** + * Determine whether the given hostname is resolvable. + * + * @param string $hostname + * @return bool + */ + protected function canResolveHostname($hostname) + { + return gethostbyname($hostname.'.') !== $hostname.'.'; + } + + /** + * Get the version that should be downloaded. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return string + */ + protected function getVersion(InputInterface $input) + { + if ($input->getOption('dev')) { + return 'dev-master'; + } + + return ''; + } + + /** + * Get the composer command for the environment. + * + * @return string + */ + protected function findComposer() + { + return implode(' ', $this->composer->findComposer()); + } + + /** + * Get the path to the appropriate PHP binary. + * + * @return string + */ + protected function phpBinary() + { + $phpBinary = (new PhpExecutableFinder)->find(false); + + return $phpBinary !== false + ? ProcessUtils::escapeArgument($phpBinary) + : 'php'; + } + + /** + * Install the given Composer Packages into the application. + * + * @return bool + */ + protected function requireComposerPackages(array $packages, OutputInterface $output, bool $asDev = false) + { + return $this->composer->requirePackages($packages, $asDev, $output); + } + + /** + * Remove the given Composer Packages from the application. + * + * @return bool + */ + protected function removeComposerPackages(array $packages, OutputInterface $output, bool $asDev = false) + { + return $this->composer->removePackages($packages, $asDev, $output); + } + + /** + * Run the given commands. + * + * @param array $commands + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @param string|null $workingPath + * @param array $env + * @return \Symfony\Component\Process\Process + */ + protected function runCommands($commands, InputInterface $input, OutputInterface $output, string $workingPath = null, array $env = []) + { + if (! $output->isDecorated()) { + $commands = array_map(function ($value) { + if (str_starts_with($value, 'chmod')) { + return $value; + } + + if (str_starts_with($value, 'git')) { + return $value; + } + + return $value.' --no-ansi'; + }, $commands); + } + + if ($input->getOption('quiet')) { + $commands = array_map(function ($value) { + if (str_starts_with($value, 'chmod')) { + return $value; + } + + if (str_starts_with($value, 'git')) { + return $value; + } + + return $value.' --quiet'; + }, $commands); + } + + $process = Process::fromShellCommandline(implode(' && ', $commands), $workingPath, $env, null, null); + + if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { + try { + $process->setTty(true); + } catch (RuntimeException $e) { + $output->writeln(' WARN '.$e->getMessage().PHP_EOL); + } + } + + $process->run(function ($type, $line) use ($output) { + $output->write(' '.$line); + }); + + return $process; + } + + /** + * Replace the given file. + * + * @param string $replace + * @param string $file + * @return void + */ + protected function replaceFile(string $replace, string $file) + { + $stubs = dirname(__DIR__).'/stubs'; + + file_put_contents( + $file, + file_get_contents("$stubs/$replace"), + ); + } + + /** + * Replace the given string in the given file. + * + * @param string|array $search + * @param string|array $replace + * @param string $file + * @return void + */ + protected function replaceInFile(string|array $search, string|array $replace, string $file) + { + file_put_contents( + $file, + str_replace($search, $replace, file_get_contents($file)) + ); + } + + /** + * Replace the given string in the given file using regular expressions. + * + * @param string|array $search + * @param string|array $replace + * @param string $file + * @return void + */ + protected function pregReplaceInFile(string $pattern, string $replace, string $file) + { + file_put_contents( + $file, + preg_replace($pattern, $replace, file_get_contents($file)) + ); + } +} diff --git a/src/NewCommandLaravel.php b/src/NewCommandLaravel.php new file mode 100644 index 0000000..69a601a --- /dev/null +++ b/src/NewCommandLaravel.php @@ -0,0 +1,897 @@ +setName('new') + ->setDescription('Create a new Laravel application') + ->addArgument('name', InputArgument::REQUIRED) + ->addOption('dev', null, InputOption::VALUE_NONE, 'Installs the latest "development" release') + ->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a Git repository') + ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The branch that should be created for a new repository', $this->defaultBranch()) + ->addOption('github', null, InputOption::VALUE_OPTIONAL, 'Create a new repository on GitHub', false) + ->addOption('organization', null, InputOption::VALUE_REQUIRED, 'The GitHub organization to create the new repository for') + ->addOption('stack', null, InputOption::VALUE_OPTIONAL, 'The Breeze / Jetstream stack that should be installed') + ->addOption('breeze', null, InputOption::VALUE_NONE, 'Installs the Laravel Breeze scaffolding') + ->addOption('jet', null, InputOption::VALUE_NONE, 'Installs the Laravel Jetstream scaffolding') + ->addOption('dark', null, InputOption::VALUE_NONE, 'Indicate whether Breeze or Jetstream should be scaffolded with dark mode support') + ->addOption('typescript', null, InputOption::VALUE_NONE, 'Indicate whether Breeze should be scaffolded with TypeScript support (Experimental)') + ->addOption('ssr', null, InputOption::VALUE_NONE, 'Indicate whether Breeze or Jetstream should be scaffolded with Inertia SSR support') + ->addOption('api', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with API support') + ->addOption('teams', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with team support') + ->addOption('verification', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with email verification support') + ->addOption('pest', null, InputOption::VALUE_NONE, 'Installs the Pest testing framework') + ->addOption('phpunit', null, InputOption::VALUE_NONE, 'Installs the PHPUnit testing framework') + ->addOption('prompt-breeze', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Breeze should be installed (Deprecated)') + ->addOption('prompt-jetstream', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Jetstream should be installed (Deprecated)') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists'); + } + + /** + * Interact with the user before validating the input. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + parent::interact($input, $output); + + $this->configurePrompts($input, $output); + + $output->write(PHP_EOL.' _ _ + | | | | + | | __ _ _ __ __ ___ _____| | + | | / _` | \'__/ _` \ \ / / _ \ | + | |___| (_| | | | (_| |\ V / __/ | + |______\__,_|_| \__,_| \_/ \___|_|'.PHP_EOL.PHP_EOL); + + if (! $input->getArgument('name')) { + $input->setArgument('name', text( + label: 'What is the name of your project?', + placeholder: 'E.g. example-app', + required: 'The project name is required.', + validate: fn ($value) => preg_match('/[^\pL\pN\-_.]/', $value) !== 0 + ? 'The name may only contain letters, numbers, dashes, underscores, and periods.' + : null, + )); + } + + if (! $input->getOption('breeze') && ! $input->getOption('jet')) { + match (select( + label: 'Would you like to install a starter kit?', + options: [ + 'none' => 'No starter kit', + 'breeze' => 'Laravel Breeze', + 'jetstream' => 'Laravel Jetstream', + ], + default: 'none', + )) { + 'breeze' => $input->setOption('breeze', true), + 'jetstream' => $input->setOption('jet', true), + default => null, + }; + } + + if ($input->getOption('breeze')) { + $this->promptForBreezeOptions($input); + } elseif ($input->getOption('jet')) { + $this->promptForJetstreamOptions($input); + } + + if (! $input->getOption('phpunit') && ! $input->getOption('pest')) { + $input->setOption('pest', select( + label: 'Which testing framework do you prefer?', + options: ['Pest', 'PHPUnit'], + default: 'Pest', + ) === 'Pest'); + } + + if (! $input->getOption('git') && $input->getOption('github') === false && Process::fromShellCommandline('git --version')->run() === 0) { + $input->setOption('git', confirm(label: 'Would you like to initialize a Git repository?', default: false)); + } + } + + /** + * Execute the command. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->validateStackOption($input); + + $name = $input->getArgument('name'); + + $directory = $name !== '.' ? getcwd().'/'.$name : '.'; + + $this->composer = new Composer(new Filesystem(), $directory); + + $version = $this->getVersion($input); + + if (! $input->getOption('force')) { + $this->verifyApplicationDoesntExist($directory); + } + + if ($input->getOption('force') && $directory === '.') { + throw new RuntimeException('Cannot use --force option when using current directory for installation!'); + } + + $composer = $this->findComposer(); + + $commands = [ + $composer." create-project laravel/laravel \"$directory\" $version --remove-vcs --prefer-dist", + ]; + + if ($directory != '.' && $input->getOption('force')) { + if (PHP_OS_FAMILY == 'Windows') { + array_unshift($commands, "(if exist \"$directory\" rd /s /q \"$directory\")"); + } else { + array_unshift($commands, "rm -rf \"$directory\""); + } + } + + if (PHP_OS_FAMILY != 'Windows') { + $commands[] = "chmod 755 \"$directory/artisan\""; + } + + if (($process = $this->runCommands($commands, $input, $output))->isSuccessful()) { + if ($name !== '.') { + $this->replaceInFile( + 'APP_URL=http://localhost', + 'APP_URL='.$this->generateAppUrl($name), + $directory.'/.env' + ); + + [$database, $migrate] = $this->promptForDatabaseOptions($directory, $input); + + $this->configureDefaultDatabaseConnection($directory, $database, $name, $migrate); + + if ($migrate) { + $this->runCommands([ + $this->phpBinary().' artisan migrate', + ], $input, $output, workingPath: $directory); + } + } + + if ($input->getOption('git') || $input->getOption('github') !== false) { + $this->createRepository($directory, $input, $output); + } + + if ($input->getOption('breeze')) { + $this->installBreeze($directory, $input, $output); + } elseif ($input->getOption('jet')) { + $this->installJetstream($directory, $input, $output); + } elseif ($input->getOption('pest')) { + $this->installPest($directory, $input, $output); + } + + if ($input->getOption('github') !== false) { + $this->pushToGitHub($name, $directory, $input, $output); + $output->writeln(''); + } + + $output->writeln(" INFO Application ready in [{$name}]. You can start your local development using:".PHP_EOL); + + $output->writeln('cd '.$name.''); + $output->writeln('php artisan serve'); + $output->writeln(''); + + $output->writeln(' New to Laravel? Check out our bootcamp and documentation. Build something amazing!'); + $output->writeln(''); + } + + return $process->getExitCode(); + } + + /** + * Return the local machine's default Git branch if set or default to `main`. + * + * @return string + */ + protected function defaultBranch() + { + $process = new Process(['git', 'config', '--global', 'init.defaultBranch']); + + $process->run(); + + $output = trim($process->getOutput()); + + return $process->isSuccessful() && $output ? $output : 'main'; + } + + /** + * Configure the default database connection. + * + * @param string $directory + * @param string $database + * @param string $name + * @param bool $migrate + * @return void + */ + protected function configureDefaultDatabaseConnection(string $directory, string $database, string $name, bool $migrate) + { + // MariaDB configuration only exists as of Laravel 11... + if ($database === 'mariadb' && ! $this->usingLaravel11OrNewer($directory)) { + $database = 'mysql'; + } + + $this->pregReplaceInFile( + '/DB_CONNECTION=.*/', + 'DB_CONNECTION='.$database, + $directory.'/.env' + ); + + $this->pregReplaceInFile( + '/DB_CONNECTION=.*/', + 'DB_CONNECTION='.$database, + $directory.'/.env.example' + ); + + if ($database === 'sqlite') { + $environment = file_get_contents($directory.'/.env'); + + // If database options aren't commented, comment them for SQLite... + if (! str_contains($environment, '# DB_HOST=127.0.0.1')) { + $this->commentDatabaseConfigurationForSqlite($directory); + + return; + } + + return; + } + + // Any commented database configuration options should be uncommented when not on SQLite... + $this->uncommentDatabaseConfiguration($directory); + + $defaultPorts = [ + 'pgsql' => '5432', + 'sqlsrv' => '1433', + ]; + + if (isset($defaultPorts[$database])) { + $this->replaceInFile( + 'DB_PORT=3306', + 'DB_PORT='.$defaultPorts[$database], + $directory.'/.env' + ); + + $this->replaceInFile( + 'DB_PORT=3306', + 'DB_PORT='.$defaultPorts[$database], + $directory.'/.env.example' + ); + } + + $this->replaceInFile( + 'DB_DATABASE=laravel', + 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), + $directory.'/.env' + ); + + $this->replaceInFile( + 'DB_DATABASE=laravel', + 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), + $directory.'/.env.example' + ); + } + + /** + * Determine if the application is using Laravel 11 or newer. + * + * @param string $directory + * @return bool + */ + public function usingLaravel11OrNewer(string $directory): bool + { + $version = json_decode(file_get_contents($directory.'/composer.json'), true)['require']['laravel/framework']; + $version = str_replace('^', '', $version); + $version = explode('.', $version)[0]; + + return $version >= 11; + } + + /** + * Comment the irrelevant database configuration entries for SQLite applications. + * + * @param string $directory + * @return void + */ + protected function commentDatabaseConfigurationForSqlite(string $directory): void + { + $defaults = [ + 'DB_HOST=127.0.0.1', + 'DB_PORT=3306', + 'DB_DATABASE=laravel', + 'DB_USERNAME=root', + 'DB_PASSWORD=', + ]; + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => "# {$default}")->all(), + $directory.'/.env' + ); + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => "# {$default}")->all(), + $directory.'/.env.example' + ); + } + + /** + * Uncomment the relevant database configuration entries for non SQLite applications. + * + * @param string $directory + * @return void + */ + protected function uncommentDatabaseConfiguration(string $directory) + { + $defaults = [ + '# DB_HOST=127.0.0.1', + '# DB_PORT=3306', + '# DB_DATABASE=laravel', + '# DB_USERNAME=root', + '# DB_PASSWORD=', + ]; + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => substr($default, 2))->all(), + $directory.'/.env' + ); + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => substr($default, 2))->all(), + $directory.'/.env.example' + ); + } + + /** + * Install Laravel Breeze into the application. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installBreeze(string $directory, InputInterface $input, OutputInterface $output) + { + $commands = array_filter([ + $this->findComposer().' require laravel/breeze', + trim(sprintf( + $this->phpBinary().' artisan breeze:install %s %s %s %s %s', + $input->getOption('stack'), + $input->getOption('typescript') ? '--typescript' : '', + $input->getOption('pest') ? '--pest' : '', + $input->getOption('dark') ? '--dark' : '', + $input->getOption('ssr') ? '--ssr' : '', + )), + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory); + + $this->commitChanges('Install Breeze', $directory, $input, $output); + } + + /** + * Install Laravel Jetstream into the application. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installJetstream(string $directory, InputInterface $input, OutputInterface $output) + { + $commands = array_filter([ + $this->findComposer().' require laravel/jetstream', + trim(sprintf( + $this->phpBinary().' artisan jetstream:install %s %s %s %s %s %s %s', + $input->getOption('stack'), + $input->getOption('api') ? '--api' : '', + $input->getOption('dark') ? '--dark' : '', + $input->getOption('teams') ? '--teams' : '', + $input->getOption('pest') ? '--pest' : '', + $input->getOption('verification') ? '--verification' : '', + $input->getOption('ssr') ? '--ssr' : '', + )), + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory); + + $this->commitChanges('Install Jetstream', $directory, $input, $output); + } + + /** + * Determine the default database connection. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return string + */ + protected function promptForDatabaseOptions(string $directory, InputInterface $input) + { + // Laravel 11.x appliations use SQLite as default... + $defaultDatabase = $this->usingLaravel11OrNewer($directory) ? 'sqlite' : 'mysql'; + + if ($input->isInteractive()) { + $database = select( + label: 'Which database will your application use?', + options: [ + 'mysql' => 'MySQL', + 'mariadb' => 'MariaDB', + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLite', + 'sqlsrv' => 'SQL Server', + ], + default: $defaultDatabase + ); + + if ($this->usingLaravel11OrNewer($directory) && $database !== $defaultDatabase) { + $migrate = confirm(label: 'Default database updated. Would you like to run the default database migrations?', default: true); + } + } + + return [$database ?? $defaultDatabase, $migrate ?? false]; + } + + /** + * Determine the stack for Breeze. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return void + */ + protected function promptForBreezeOptions(InputInterface $input) + { + if (! $input->getOption('stack')) { + $input->setOption('stack', select( + label: 'Which Breeze stack would you like to install?', + options: [ + 'blade' => 'Blade with Alpine', + 'livewire' => 'Livewire (Volt Class API) with Alpine', + 'livewire-functional' => 'Livewire (Volt Functional API) with Alpine', + 'react' => 'React with Inertia', + 'vue' => 'Vue with Inertia', + 'api' => 'API only', + ], + default: 'blade', + )); + } + + if (in_array($input->getOption('stack'), ['react', 'vue']) && (! $input->getOption('dark') || ! $input->getOption('ssr'))) { + collect(multiselect( + label: 'Would you like any optional features?', + options: [ + 'dark' => 'Dark mode', + 'ssr' => 'Inertia SSR', + 'typescript' => 'TypeScript (experimental)', + ], + default: array_filter([ + $input->getOption('dark') ? 'dark' : null, + $input->getOption('ssr') ? 'ssr' : null, + $input->getOption('typescript') ? 'typescript' : null, + ]), + ))->each(fn ($option) => $input->setOption($option, true)); + } elseif (in_array($input->getOption('stack'), ['blade', 'livewire', 'livewire-functional']) && ! $input->getOption('dark')) { + $input->setOption('dark', confirm( + label: 'Would you like dark mode support?', + default: false, + )); + } + } + + /** + * Determine the stack for Jetstream. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return void + */ + protected function promptForJetstreamOptions(InputInterface $input) + { + if (! $input->getOption('stack')) { + $input->setOption('stack', select( + label: 'Which Jetstream stack would you like to install?', + options: [ + 'livewire' => 'Livewire', + 'inertia' => 'Vue with Inertia', + ], + default: 'livewire', + )); + } + + collect(multiselect( + label: 'Would you like any optional features?', + options: collect([ + 'api' => 'API support', + 'dark' => 'Dark mode', + 'verification' => 'Email verification', + 'teams' => 'Team support', + ])->when( + $input->getOption('stack') === 'inertia', + fn ($options) => $options->put('ssr', 'Inertia SSR') + )->all(), + default: array_filter([ + $input->getOption('api') ? 'api' : null, + $input->getOption('dark') ? 'dark' : null, + $input->getOption('teams') ? 'teams' : null, + $input->getOption('verification') ? 'verification' : null, + $input->getOption('stack') === 'inertia' && $input->getOption('ssr') ? 'ssr' : null, + ]), + ))->each(fn ($option) => $input->setOption($option, true)); + } + + /** + * Validate the starter kit stack input. + * + * @param \Symfony\Components\Console\Input\InputInterface + */ + protected function validateStackOption(InputInterface $input) + { + if ($input->getOption('breeze')) { + if (! in_array($input->getOption('stack'), $stacks = ['blade', 'livewire', 'livewire-functional', 'react', 'vue', 'api'])) { + throw new \InvalidArgumentException("Invalid Breeze stack [{$input->getOption('stack')}]. Valid options are: ".implode(', ', $stacks).'.'); + } + + return; + } + + if ($input->getOption('jet')) { + if (! in_array($input->getOption('stack'), $stacks = ['inertia', 'livewire'])) { + throw new \InvalidArgumentException("Invalid Jetstream stack [{$input->getOption('stack')}]. Valid options are: ".implode(', ', $stacks).'.'); + } + + return; + } + } + + /** + * Install Pest into the application. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installPest(string $directory, InputInterface $input, OutputInterface $output) + { + if ($this->removeComposerPackages(['phpunit/phpunit', '--no-update'], $output, true) + && $this->requireComposerPackages(['pestphp/pest:^2.0', 'pestphp/pest-plugin-laravel:^2.0'], $output, true)) { + $commands = array_filter([ + $this->phpBinary().' ./vendor/bin/pest --init', + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory, env: [ + 'PEST_NO_SUPPORT' => 'true', + ]); + + $this->replaceFile( + 'pest/Feature.php', + $directory.'/tests/Feature/ExampleTest.php', + ); + + $this->replaceFile( + 'pest/Unit.php', + $directory.'/tests/Unit/ExampleTest.php', + ); + + $this->commitChanges('Install Pest', $directory, $input, $output); + } + } + + /** + * Create a Git repository and commit the base Laravel skeleton. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function createRepository(string $directory, InputInterface $input, OutputInterface $output) + { + $branch = $input->getOption('branch') ?: $this->defaultBranch(); + + $commands = [ + 'git init -q', + 'git add .', + 'git commit -q -m "Set up a fresh Laravel app"', + "git branch -M {$branch}", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory); + } + + /** + * Commit any changes in the current working directory. + * + * @param string $message + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function commitChanges(string $message, string $directory, InputInterface $input, OutputInterface $output) + { + if (! $input->getOption('git') && $input->getOption('github') === false) { + return; + } + + $commands = [ + 'git add .', + "git commit -q -m \"$message\"", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory); + } + + /** + * Create a GitHub repository and push the git log to it. + * + * @param string $name + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function pushToGitHub(string $name, string $directory, InputInterface $input, OutputInterface $output) + { + $process = new Process(['gh', 'auth', 'status']); + $process->run(); + + if (! $process->isSuccessful()) { + $output->writeln(' WARN Make sure the "gh" CLI tool is installed and that you\'re authenticated to GitHub. Skipping...'.PHP_EOL); + + return; + } + + $name = $input->getOption('organization') ? $input->getOption('organization')."/$name" : $name; + $flags = $input->getOption('github') ?: '--private'; + + $commands = [ + "gh repo create {$name} --source=. --push {$flags}", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory, env: ['GIT_TERMINAL_PROMPT' => 0]); + } + + /** + * Verify that the application does not already exist. + * + * @param string $directory + * @return void + */ + protected function verifyApplicationDoesntExist($directory) + { + if ((is_dir($directory) || is_file($directory)) && $directory != getcwd()) { + throw new RuntimeException('Application already exists!'); + } + } + + /** + * Generate a valid APP_URL for the given application name. + * + * @param string $name + * @return string + */ + protected function generateAppUrl($name) + { + $hostname = mb_strtolower($name).'.test'; + + return $this->canResolveHostname($hostname) ? 'http://'.$hostname : 'http://localhost'; + } + + /** + * Determine whether the given hostname is resolvable. + * + * @param string $hostname + * @return bool + */ + protected function canResolveHostname($hostname) + { + return gethostbyname($hostname.'.') !== $hostname.'.'; + } + + /** + * Get the version that should be downloaded. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return string + */ + protected function getVersion(InputInterface $input) + { + if ($input->getOption('dev')) { + return 'dev-master'; + } + + return ''; + } + + /** + * Get the composer command for the environment. + * + * @return string + */ + protected function findComposer() + { + return implode(' ', $this->composer->findComposer()); + } + + /** + * Get the path to the appropriate PHP binary. + * + * @return string + */ + protected function phpBinary() + { + $phpBinary = (new PhpExecutableFinder)->find(false); + + return $phpBinary !== false + ? ProcessUtils::escapeArgument($phpBinary) + : 'php'; + } + + /** + * Install the given Composer Packages into the application. + * + * @return bool + */ + protected function requireComposerPackages(array $packages, OutputInterface $output, bool $asDev = false) + { + return $this->composer->requirePackages($packages, $asDev, $output); + } + + /** + * Remove the given Composer Packages from the application. + * + * @return bool + */ + protected function removeComposerPackages(array $packages, OutputInterface $output, bool $asDev = false) + { + return $this->composer->removePackages($packages, $asDev, $output); + } + + /** + * Run the given commands. + * + * @param array $commands + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @param string|null $workingPath + * @param array $env + * @return \Symfony\Component\Process\Process + */ + protected function runCommands($commands, InputInterface $input, OutputInterface $output, string $workingPath = null, array $env = []) + { + if (! $output->isDecorated()) { + $commands = array_map(function ($value) { + if (str_starts_with($value, 'chmod')) { + return $value; + } + + if (str_starts_with($value, 'git')) { + return $value; + } + + return $value.' --no-ansi'; + }, $commands); + } + + if ($input->getOption('quiet')) { + $commands = array_map(function ($value) { + if (str_starts_with($value, 'chmod')) { + return $value; + } + + if (str_starts_with($value, 'git')) { + return $value; + } + + return $value.' --quiet'; + }, $commands); + } + + $process = Process::fromShellCommandline(implode(' && ', $commands), $workingPath, $env, null, null); + + if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { + try { + $process->setTty(true); + } catch (RuntimeException $e) { + $output->writeln(' WARN '.$e->getMessage().PHP_EOL); + } + } + + $process->run(function ($type, $line) use ($output) { + $output->write(' '.$line); + }); + + return $process; + } + + /** + * Replace the given file. + * + * @param string $replace + * @param string $file + * @return void + */ + protected function replaceFile(string $replace, string $file) + { + $stubs = dirname(__DIR__).'/stubs'; + + file_put_contents( + $file, + file_get_contents("$stubs/$replace"), + ); + } + + /** + * Replace the given string in the given file. + * + * @param string|array $search + * @param string|array $replace + * @param string $file + * @return void + */ + protected function replaceInFile(string|array $search, string|array $replace, string $file) + { + file_put_contents( + $file, + str_replace($search, $replace, file_get_contents($file)) + ); + } + + /** + * Replace the given string in the given file using regular expressions. + * + * @param string|array $search + * @param string|array $replace + * @param string $file + * @return void + */ + protected function pregReplaceInFile(string $pattern, string $replace, string $file) + { + file_put_contents( + $file, + preg_replace($pattern, $replace, file_get_contents($file)) + ); + } +} \ No newline at end of file diff --git a/stubs/pest/Feature.php b/stubs/pest/Feature.php new file mode 100644 index 0000000..8b5843f --- /dev/null +++ b/stubs/pest/Feature.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); +}); diff --git a/stubs/pest/Unit.php b/stubs/pest/Unit.php new file mode 100644 index 0000000..44a4f33 --- /dev/null +++ b/stubs/pest/Unit.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests-output/.gitignore b/tests-output/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests-output/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/NewCommandTest.php b/tests/NewCommandTest.php new file mode 100644 index 0000000..43ddf07 --- /dev/null +++ b/tests/NewCommandTest.php @@ -0,0 +1,49 @@ +add(new NewCommand); + + $tester = new CommandTester($app->find('new')); + + $statusCode = $tester->execute(['name' => $scaffoldDirectoryName], ['interactive' => false]); + + $this->assertSame(0, $statusCode); + $this->assertDirectoryExists($scaffoldDirectory.'/vendor'); + $this->assertFileExists($scaffoldDirectory.'/.env'); + } + + public function test_on_at_least_laravel_11() + { + $command = new NewCommand; + + $onLaravel10 = $command->usingLaravel11OrNewer(__DIR__.'/fixtures/laravel10'); + $onLaravel11 = $command->usingLaravel11OrNewer(__DIR__.'/fixtures/laravel11'); + $onLaravel12 = $command->usingLaravel11OrNewer(__DIR__.'/fixtures/laravel12'); + + $this->assertFalse($onLaravel10); + $this->assertTrue($onLaravel11); + $this->assertTrue($onLaravel12); + } +} diff --git a/tests/fixtures/laravel10/composer.json b/tests/fixtures/laravel10/composer.json new file mode 100644 index 0000000..8a3d72d --- /dev/null +++ b/tests/fixtures/laravel10/composer.json @@ -0,0 +1,66 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The skeleton application for the Laravel framework.", + "keywords": ["laravel", "framework"], + "license": "MIT", + "require": { + "php": "^8.1", + "guzzlehttp/guzzle": "^7.2", + "laravel/framework": "^10.10", + "laravel/sanctum": "^3.3", + "laravel/tinker": "^2.8" + }, + "require-dev": { + "fakerphp/faker": "^1.9.1", + "laravel/pint": "^1.0", + "laravel/sail": "^1.18", + "mockery/mockery": "^1.4.4", + "nunomaduro/collision": "^7.0", + "phpunit/phpunit": "^10.1", + "spatie/laravel-ignition": "^2.0" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi" + ] + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/tests/fixtures/laravel11/composer.json b/tests/fixtures/laravel11/composer.json new file mode 100644 index 0000000..3cdf16e --- /dev/null +++ b/tests/fixtures/laravel11/composer.json @@ -0,0 +1,69 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The skeleton application for the Laravel framework.", + "keywords": ["laravel", "framework"], + "license": "MIT", + "require": { + "php": "^8.2", + "laravel/framework": "^11.0", + "laravel/tinker": "^2.9" + }, + "require-dev": { + "fakerphp/faker": "^1.23", + "laravel/pint": "^1.13", + "laravel/sail": "^1.26", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.0", + "phpunit/phpunit": "^10.5", + "spatie/laravel-ignition": "^2.4" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi", + "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", + "@php artisan migrate --ansi" + ] + }, + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + }, + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/tests/fixtures/laravel12/composer.json b/tests/fixtures/laravel12/composer.json new file mode 100644 index 0000000..7181145 --- /dev/null +++ b/tests/fixtures/laravel12/composer.json @@ -0,0 +1,69 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The skeleton application for the Laravel framework.", + "keywords": ["laravel", "framework"], + "license": "MIT", + "require": { + "php": "^8.2", + "laravel/framework": "^12.0.1", + "laravel/tinker": "^2.9" + }, + "require-dev": { + "fakerphp/faker": "^1.23", + "laravel/pint": "^1.13", + "laravel/sail": "^1.26", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.0", + "phpunit/phpunit": "^10.5", + "spatie/laravel-ignition": "^2.4" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi", + "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", + "@php artisan migrate --ansi" + ] + }, + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + }, + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true +}