From 22e8386d31d58acd3685f8703a4dfc9da5a27b1b Mon Sep 17 00:00:00 2001 From: Zenlunaris Date: Mon, 19 Aug 2024 15:12:27 +0700 Subject: [PATCH] fork --- .editorconfig | 15 + .gitattributes | 19 + .gitignore | 4 + .styleci.yml | 4 + CHANGELOG.md | 424 +++++++++++ LICENSE.md | 21 + README.md | 26 + RELEASE.md | 4 + bin/laravel | 13 + composer.json | 42 ++ phpstan.neon.dist | 6 + phpunit.xml.dist | 9 + src/Concerns/ConfiguresPrompts.php | 132 ++++ src/NewCommand.php | 986 +++++++++++++++++++++++++ stubs/pest/Feature.php | 7 + stubs/pest/Unit.php | 5 + tests-output/.gitignore | 2 + tests/NewCommandTest.php | 49 ++ tests/fixtures/laravel10/composer.json | 66 ++ tests/fixtures/laravel11/composer.json | 69 ++ tests/fixtures/laravel12/composer.json | 69 ++ 21 files changed, 1972 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .styleci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 RELEASE.md create mode 100644 bin/laravel create mode 100644 composer.json create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 src/Concerns/ConfiguresPrompts.php create mode 100644 src/NewCommand.php create mode 100644 stubs/pest/Feature.php create mode 100644 stubs/pest/Unit.php create mode 100644 tests-output/.gitignore create mode 100644 tests/NewCommandTest.php create mode 100644 tests/fixtures/laravel10/composer.json create mode 100644 tests/fixtures/laravel11/composer.json create mode 100644 tests/fixtures/laravel12/composer.json 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/.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..bc2e575 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,424 @@ +# Release Notes + +## [Unreleased](https://github.com/laravel/installer/compare/v5.8.3...master) + +## [v5.8.3](https://github.com/laravel/installer/compare/v5.8.2...v5.8.3) - 2024-06-18 + +* Validate Laravel can be installed on selected directory by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/installer/pull/344 +* Automatically run migration when `--database` is given. by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/installer/pull/346 + +## [v5.8.2](https://github.com/laravel/installer/compare/v5.8.1...v5.8.2) - 2024-06-02 + +* [5.x] Fixes issue if there is no `herd` or `valet` installed by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/installer/pull/343 + +## [v5.8.1](https://github.com/laravel/installer/compare/v5.8.0...v5.8.1) - 2024-05-21 + +* Only run migration after selecting the database by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/installer/pull/337 +* [1.x] Enforce output when running with `--quiet` by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/installer/pull/339 + +## [v5.8.0](https://github.com/laravel/installer/compare/v5.7.3...v5.8.0) - 2024-05-07 + +* Improves output if Herd or Valet are installed. by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/installer/pull/335 +* Gets the default "tld" on install. by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/installer/pull/336 + +## [v5.7.3](https://github.com/laravel/installer/compare/v5.7.2...v5.7.3) - 2024-04-30 + +* Prevent using unavailable databases by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/installer/pull/334 + +## [v5.7.2](https://github.com/laravel/installer/compare/v5.7.1...v5.7.2) - 2024-04-16 + +* Install Breeze with dev flag by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/installer/pull/332 + +## [v5.7.1](https://github.com/laravel/installer/compare/v5.7.0...v5.7.1) - 2024-04-02 + +* [5.x] Removes "(experimental)" label by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/330 + +## [v5.7.0](https://github.com/laravel/installer/compare/v5.6.0...v5.7.0) - 2024-03-26 + +* Remove unused `migrate` parameter by [@nshiro](https://github.com/nshiro) in https://github.com/laravel/installer/pull/323 +* Add `--database` option by [@CasEbb](https://github.com/CasEbb) in https://github.com/laravel/installer/pull/320 + +## [v5.6.0](https://github.com/laravel/installer/compare/v5.5.3...v5.6.0) - 2024-03-12 + +* Remove Laravel 11 Checks by [@Jubeki](https://github.com/Jubeki) in https://github.com/laravel/installer/pull/319 + +## [v5.5.3](https://github.com/laravel/installer/compare/v5.5.2...v5.5.3) - 2024-03-05 + +* [11.x] Removes `publish config` files by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/317 + +## [v5.5.2](https://github.com/laravel/installer/compare/v5.5.1...v5.5.2) - 2024-02-20 + +- Prompt for config publish by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/installer/commit/c3b3c661ffc1323951537b5ff470ec706357fa1f + +## [v5.5.1](https://github.com/laravel/installer/compare/v5.5.0...v5.5.1) - 2024-02-12 + +* [5.x] Fixes Pest installation on Windows by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/314 + +## [v5.5.0](https://github.com/laravel/installer/compare/v5.4.0...v5.5.0) - 2024-01-30 + +* [5.x] Improves installation of Pest by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/installer/pull/311 + +## [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/laravel b/bin/laravel new file mode 100644 index 0000000..99fa159 --- /dev/null +++ b/bin/laravel @@ -0,0 +1,13 @@ +#!/usr/bin/env php +add(new Laravel\Installer\Console\NewCommand); + +$app->run(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..87921fc --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "laravel/installer", + "description": "Laravel application installer.", + "keywords": ["laravel"], + "license": "MIT", + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "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/laravel" + ], + "autoload": { + "psr-4": { + "Laravel\\Installer\\Console\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Laravel\\Installer\\Console\\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..6cb7570 --- /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..dd0814a --- /dev/null +++ b/src/NewCommand.php @@ -0,0 +1,986 @@ +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('database', null, InputOption::VALUE_REQUIRED, 'The database driver your application will use') + ->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') + ->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: function ($value) use ($input) { + if (preg_match('/[^\pL\pN\-_.]/', $value) !== 0) { + return 'The name may only contain letters, numbers, dashes, underscores, and periods.'; + } + + if ($input->getOption('force') !== true) { + try { + $this->verifyApplicationDoesntExist($this->getInstallationDirectory($value)); + } catch (RuntimeException $e) { + return 'Application already exists.'; + } + } + }, + )); + } + + if ($input->getOption('force') !== true) { + $this->verifyApplicationDoesntExist( + $this->getInstallationDirectory($input->getArgument('name')) + ); + } + + 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->validateDatabaseOption($input); + $this->validateStackOption($input); + + $name = $input->getArgument('name'); + + $directory = $this->getInstallationDirectory($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(); + $phpBinary = $this->phpBinary(); + + $commands = [ + $composer." create-project laravel/laravel \"$directory\" $version --remove-vcs --prefer-dist --no-scripts", + $composer." run post-root-package-install -d \"$directory\"", + $phpBinary." \"$directory/artisan\" key:generate --ansi", + ]; + + 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); + + 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.''); + + if ($this->isParked($directory)) { + $url = $this->generateAppUrl($name); + $output->writeln('➜ Open: '.$url.''); + } else { + $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 + * @return void + */ + protected function configureDefaultDatabaseConnection(string $directory, string $database, string $name) + { + $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 usingLaravelVersionOrNewer(int $usingVersion, 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 >= $usingVersion; + } + + /** + * 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 --dev', + 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 array + */ + protected function promptForDatabaseOptions(string $directory, InputInterface $input) + { + $defaultDatabase = collect( + $databaseOptions = $this->databaseOptions() + )->keys()->first(); + + if (! $input->getOption('database') && $input->isInteractive()) { + $input->setOption('database', select( + label: 'Which database will your application use?', + options: $databaseOptions, + default: $defaultDatabase, + )); + + $migrate = confirm( + label: $input->getOption('database') !== 'sqlite' + ? 'Default database updated. Would you like to run the default database migrations?' + : 'Would you like to run the default database migrations?', + default: true + ); + } + + return [$input->getOption('database') ?? $defaultDatabase, $migrate ?? $input->hasOption('database')]; + } + + /** + * Get the available database options. + * + * @return array + */ + protected function databaseOptions(): array + { + return collect([ + 'sqlite' => ['SQLite', extension_loaded('pdo_sqlite')], + 'mysql' => ['MySQL', extension_loaded('pdo_mysql')], + 'mariadb' => ['MariaDB', extension_loaded('pdo_mysql')], + 'pgsql' => ['PostgreSQL', extension_loaded('pdo_pgsql')], + 'sqlsrv' => ['SQL Server', extension_loaded('pdo_sqlsrv')], + ]) + ->sortBy(fn ($database) => $database[1] ? 0 : 1) + ->map(fn ($database) => $database[0].($database[1] ? '' : ' (Missing PDO extension)')) + ->all(); + } + + /** + * Determine the stack for Breeze. + * + * @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', + ], + 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 database driver input. + * + * @param \Symfony\Components\Console\Input\InputInterface + */ + protected function validateDatabaseOption(InputInterface $input) + { + if ($input->getOption('database') && ! in_array($input->getOption('database'), $drivers = ['mysql', 'mariadb', 'pgsql', 'sqlite', 'sqlsrv'])) { + throw new \InvalidArgumentException("Invalid database driver [{$input->getOption('database')}]. Valid options are: ".implode(', ', $drivers).'.'); + } + } + + /** + * 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) + { + $composerBinary = $this->findComposer(); + + $commands = [ + $composerBinary.' remove phpunit/phpunit --dev --no-update', + $composerBinary.' require pestphp/pest pestphp/pest-plugin-laravel --no-update --dev', + $composerBinary.' update', + $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).'.'.$this->getTld(); + + return $this->canResolveHostname($hostname) ? 'http://'.$hostname : 'http://localhost'; + } + + /** + * Get the TLD for the application. + * + * @return string + */ + protected function getTld() + { + return $this->runOnValetOrHerd('tld') ?? 'test'; + } + + /** + * Determine whether the given hostname is resolvable. + * + * @param string $hostname + * @return bool + */ + protected function canResolveHostname($hostname) + { + return gethostbyname($hostname.'.') !== $hostname.'.'; + } + + /** + * Determine if the given directory is parked using Herd or Valet. + * + * @param string $directory + * @return bool + */ + protected function isParked(string $directory) + { + $output = $this->runOnValetOrHerd('paths'); + + return $output !== false ? in_array(dirname($directory), json_decode($output)) : false; + } + + /** + * Get the installation directory. + * + * @param string $name + * @return string + */ + protected function getInstallationDirectory(string $name) + { + return $name !== '.' ? getcwd().'/'.$name : '.'; + } + + /** + * 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'; + } + + /** + * Runs the given command on the "herd" or "valet" CLI. + * + * @param string $command + * @return string|bool + */ + protected function runOnValetOrHerd(string $command) + { + foreach (['herd', 'valet'] as $tool) { + $process = new Process([$tool, $command, '-v']); + + try { + $process->run(); + + if ($process->isSuccessful()) { + return trim($process->getOutput()); + } + } catch (ProcessStartFailedException) { + } + } + + return false; + } + + /** + * 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/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..2020305 --- /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->usingLaravelVersionOrNewer(11, __DIR__.'/fixtures/laravel10'); + $onLaravel11 = $command->usingLaravelVersionOrNewer(11, __DIR__.'/fixtures/laravel11'); + $onLaravel12 = $command->usingLaravelVersionOrNewer(11, __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 +}