commit 22e8386d31d58acd3685f8703a4dfc9da5a27b1b Author: Zenlunaris Date: Mon Aug 19 15:12:27 2024 +0700 fork 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 +}