├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── alchemy.yml ├── bin └── leaf ├── composer.json └── src ├── CreateCommand.php ├── InstallCommand.php ├── InteractCommand.php ├── RunCommand.php ├── ServeCommand.php ├── UICommand.php ├── UninstallCommand.php ├── UpdateCommand.php ├── Utils ├── Core.php └── Package.php ├── ViewBuildCommand.php ├── ViewInstallCommand.php ├── themes ├── api │ └── routes │ │ ├── _app.php │ │ └── index.php ├── bareui │ ├── setup │ │ └── _bareui.php │ └── views │ │ └── index.view.php ├── blade │ ├── setup │ │ └── _blade.php │ └── views │ │ └── index.blade.php ├── docker │ ├── docker-compose.yml │ └── docker │ │ ├── 000-default.conf │ │ ├── Dockerfile │ │ └── php.ini ├── inertia │ ├── root │ │ └── vite.config.js │ └── views │ │ ├── bare-ui │ │ └── _inertia.view.php │ │ └── blade │ │ └── _inertia.blade.php ├── leaf3 │ ├── .htaccess │ ├── composer.json │ ├── index.php │ └── welcome.html ├── mvc │ └── docker │ │ ├── docker-compose.yml │ │ └── docker │ │ ├── 000-default.conf │ │ ├── Dockerfile │ │ └── php.ini ├── react │ ├── routes │ │ └── _frontend.php │ ├── views │ │ ├── _inertia.blade.php │ │ └── js │ │ │ ├── Pages │ │ │ ├── Hello.jsx │ │ │ └── Login.jsx │ │ │ └── app.jsx │ └── vite.config.js ├── svelte │ ├── routes │ │ └── _frontend.php │ ├── views │ │ ├── _inertia.blade.php │ │ └── js │ │ │ ├── Pages │ │ │ └── Hello.svelte │ │ │ └── app.js │ └── vite.config.js ├── tailwind │ ├── view │ │ └── css │ │ │ └── app.css │ └── vite.config.js ├── vite │ └── vite.config.js └── vue │ ├── routes │ └── _frontend.php │ ├── views │ ├── _inertia.blade.php │ └── js │ │ ├── Pages │ │ └── Hello.vue │ │ └── app.js │ └── vite.config.js └── ui ├── .editorconfig ├── .eslintrc.cjs ├── .gitignore ├── composer.json ├── dist ├── assets │ ├── index-9259d9ea.css │ └── index-cc548c58.js ├── index.html ├── server.php └── vite.svg ├── index.html ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── @types │ │ ├── DirectoryInput.ts │ │ ├── InlineForm.ts │ │ └── PageLayout.ts │ ├── Card.tsx │ ├── ConsoleCard.tsx │ ├── DirectoryInput.tsx │ ├── InlineForm.tsx │ ├── Nav.tsx │ └── PageLayout.tsx ├── data │ └── walkthrough.tsx ├── index.css ├── main.tsx ├── pages │ ├── @types │ │ └── CreateScreen.ts │ ├── AppsScreen.tsx │ ├── Create │ │ ├── AdditionalFrontendOptionsScreen.tsx │ │ ├── AppTypeScreen.tsx │ │ ├── DockerScreen.tsx │ │ ├── FrontendFrameworkScreen.tsx │ │ ├── ModulesScreen.tsx │ │ ├── NameScreen.tsx │ │ ├── ReviewScreen.tsx │ │ ├── TemplateEngineScreen.tsx │ │ └── TestingScreen.tsx │ ├── CreateScreen.tsx │ ├── HomeScreen.tsx │ ├── InsightsScreen.tsx │ ├── LeafNotFound.tsx │ ├── LoadingScreen.tsx │ └── RoutesScreen.tsx ├── utils │ ├── fs.tsx │ └── router.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── ui.config.json ├── vite.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false 13 | quote_type = double 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: leaf 4 | github: leafsphp 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | phpunit.xml 4 | .phpunit.result.cache 5 | .idea 6 | test 7 | .DS_Store 8 | 9 | # Alchemy 10 | .alchemy 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michael Darko-Duodu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 |

4 | 5 |
6 |

7 | 8 |

Leaf CLI 2

9 | 10 |

11 | Latest Stable Version 16 | Total Downloads 21 | License 26 |

27 |
28 |
29 | 30 | A simple command line tool for creating and interacting with your leaf projects. You can do stuff like installing packages, interacting with your app, previewing your app, ... 31 | 32 | ## Installation 33 | 34 | You can get this tool up and running on your system using composer: 35 | 36 | ```bash 37 | composer global require leafs/cli 38 | ``` 39 | 40 | Make sure to place Composer's system-wide vendor bin directory in your `$PATH` so the leaf executable can be located by your system. This directory exists in different locations based on your operating system; however, some common locations include: 41 | 42 | - Windows: `%USERPROFILE%\AppData\Roaming\Composer\vendor\bin` 43 | - macOS: `$HOME/.composer/vendor/bin` 44 | - GNU / Linux Distributions: `$HOME/.config/composer/vendor/bin` or `$HOME/.composer/vendor/bin` 45 | 46 | You could also find the composer's global installation path by running `composer global about` and looking up from the first line. 47 | 48 | Eg (Adding composer bin to path linux): 49 | 50 | ```sh 51 | export PATH=$PATH:$HOME/.config/composer/vendor/bin 52 | ``` 53 | 54 | Eg (Adding composer bin to path mac): 55 | 56 | ```sh 57 | export PATH=$PATH:$HOME/.composer/vendor/bin 58 | echo $PATH 59 | ``` 60 | 61 | ## Usage Guide 62 | 63 | ### Creating projects 64 | 65 | To start a new project, simply open up your console or terminal in your directory 66 | for projects and enter: 67 | 68 | With leaf 3: 69 | 70 | ```sh 71 | leaf create 72 | ``` 73 | 74 | This will now prompt you to select a preset 75 | 76 | ```sh 77 | Creating a new Leaf app "" in ./projects-directory. 78 | 79 | * Please pick a preset 80 | [0] leaf 81 | [1] leaf mvc 82 | [2] leaf api 83 | > 84 | ``` 85 | 86 | Selecting a number will generate a leaf app based on the associated preset. As you can see, there are 3 presets: 87 | 88 | - **Leaf**: a bare leaf 3 project 89 | - **Leaf MVC**: a leaf MVC project with leaf 3 90 | - **Leaf API**: a leaf API project with leaf 3 91 | 92 | You can also pick a preset directly without going through the interactive installer. 93 | 94 | **Leaf:** 95 | 96 | ```bash 97 | leaf create --basic 98 | ``` 99 | 100 | **Leaf API:** 101 | 102 | ```bash 103 | leaf create --api 104 | ``` 105 | 106 | **Leaf MVC:** 107 | 108 | ```bash 109 | leaf create --mvc 110 | ``` 111 | 112 | You can also add `--custom` for a fully customisable leaf project. 113 | 114 | ```bash 115 | leaf create --custom 116 | ``` 117 | 118 | ### Installing packages 119 | 120 | This cli tool also adds a feature to install modules from composer 121 | 122 | ```sh 123 | leaf install ui 124 | ``` 125 | 126 | This installs the `leafs/ui` package. 127 | 128 | You can also install third party packages from packagist 129 | 130 | ```sh 131 | leaf install psr/log 132 | ``` 133 | 134 | ### Interactive Shell 135 | 136 | You can also use the interactive shell to interact with your app. 137 | 138 | ```bash 139 | $ leaf interact 140 | ... 141 | >>> $user = new User; 142 | ... 143 | >>> $user->name = "Mychi"; 144 | ... 145 | >>> $user->save(); 146 | ``` 147 | 148 | ### Previewing your app 149 | 150 | This opens up your app on the PHP local server. 151 | 152 | ```sh 153 | leaf serve 154 | ``` 155 | 156 | You can also specify the port 157 | 158 | ```bash 159 | leaf serve -p 8000 160 | ``` 161 | 162 | In v2.1, you can also start the leaf server with hot module watching. This reloads your application anytime a change is made to your application code. To get started, simply start the leaf server with the `--watch` flag. 163 | 164 | ```sh 165 | leaf serve --port 8000 --watch 166 | ``` 167 | 168 | ## License 169 | 170 | Leaf CLI is open-sourced software licensed under the [MIT license](LICENSE.md). 171 | 172 | ## 😇 Contributing 173 | 174 | We are glad to have you. All contributions are welcome! To get started, familiarize yourself with our [contribution guide](https://leafphp.dev/community/contributing.html) and you'll be ready to make your first pull request 🚀. 175 | 176 | To report a security vulnerability, you can reach out to [@mychidarko](https://twitter.com/mychidarko) or [@leafphp](https://twitter.com/leafphp) on twitter. We will coordinate the fix and eventually commit the solution in this project. 177 | 178 | ### Code contributors 179 | 180 | 181 | 182 | 191 | 200 | 201 |
183 | 184 | 185 |
186 | 187 | Michael Darko 188 | 189 |
190 |
192 | 193 | 194 |
195 | 196 | tedtop 197 | 198 |
199 |
202 | 203 | ## Sponsoring Leaf 204 | 205 | Your cash contributions go a long way to help us make Leaf even better for you. You can sponsor Leaf and any of our packages on [open collective](https://opencollective.com/leaf) or check the [contribution page](https://leafphp.dev/support/) for a list of ways to contribute. 206 | 207 | And to all our existing cash/code contributors, we love you all ❤️ 208 | 209 | ### Cash contributors 210 | 211 | You can view all sponsors @ [https://leafphp.dev/#sponsors](https://leafphp.dev/#sponsors) 212 | 213 | ## 🤯 Links/Projects 214 | 215 | - [Leaf Docs](https://leafphp.dev) 216 | - [Leaf MVC](https://mvc.leafphp.dev) 217 | - [Leaf API](https://api.leafphp.dev) 218 | - [Leaf CLI](https://cli.leafphp.dev) 219 | - [Aloe CLI](https://leafphp.dev/aloe-cli/) 220 | -------------------------------------------------------------------------------- /alchemy.yml: -------------------------------------------------------------------------------- 1 | app: 2 | - bin 3 | - src 4 | 5 | lint: 6 | preset: PSR12 7 | rules: 8 | single_quote: true 9 | phpdoc_scalar: true 10 | no_unused_imports: true 11 | unary_operator_spaces: true 12 | binary_operator_spaces: true 13 | phpdoc_var_without_name: true 14 | trailing_comma_in_multiline: true 15 | phpdoc_single_line_var_spacing: true 16 | single_trait_insert_per_statement: true 17 | not_operator_with_successor_space: false 18 | array_syntax: 19 | syntax: short 20 | ordered_imports: 21 | sort_algorithm: alpha 22 | method_argument_space: 23 | on_multiline: ensure_fully_multiline 24 | keep_multiple_spaces_after_comma: true 25 | blank_line_before_statement: 26 | statements: 27 | - try 28 | - break 29 | - throw 30 | - return 31 | - declare 32 | - continue 33 | 34 | actions: 35 | run: 36 | - lint 37 | os: 38 | - ubuntu-latest 39 | php: 40 | versions: 41 | - '8.3' 42 | events: 43 | - push 44 | - pull_request 45 | -------------------------------------------------------------------------------- /bin/leaf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | version; 12 | 13 | $app = sprout()->createApp([ 14 | 'name' => " 15 | _ __ ___ _ ___ 16 | | | ___ __ _ / _| / __| | |_ \033[1;33m$currentVersion\033[0m 17 | | |__/ -_) _` | _| | (__| |__ | | 18 | |____\___\__,_|_| \___|____|___| 19 | ", 20 | 'version' => '' 21 | ]); 22 | 23 | $app->register(Leaf\Console\CreateCommand::class); 24 | // $app->register(Leaf\Console\UICommand::class); 25 | $app->register(Leaf\Console\UpdateCommand::class); 26 | $app->register(Leaf\Console\InstallCommand::class); 27 | $app->register(Leaf\Console\UninstallCommand::class); 28 | $app->register(Leaf\Console\ServeCommand::class); 29 | $app->register(Leaf\Console\InteractCommand::class); 30 | $app->register(Leaf\Console\RunCommand::class); 31 | $app->register(Leaf\Console\ViewBuildCommand::class); 32 | $app->register(Leaf\Console\ViewInstallCommand::class); 33 | 34 | $app->run(); 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leafs/cli", 3 | "description": "A simple command line tool for installing and interacting with your leaf apps", 4 | "homepage": "https://cli.leafphp.dev", 5 | "version": "v4.3", 6 | "keywords": [ 7 | "leaf", 8 | "php", 9 | "installer", 10 | "deploy", 11 | "server", 12 | "database" 13 | ], 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Michael Darko", 18 | "email": "mickdd22@gmail.com", 19 | "homepage": "https://mychi.netlify.app", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "ext-json": "*", 25 | "leafs/fs": "^4.1", 26 | "leafs/sprout": "^0.4.0", 27 | "psy/psysh": "*" 28 | }, 29 | "bin": [ 30 | "bin/leaf" 31 | ], 32 | "autoload": { 33 | "psr-4": { 34 | "Leaf\\Console\\": "src/" 35 | } 36 | }, 37 | "config": { 38 | "sort-packages": true, 39 | "allow-plugins": { 40 | "pestphp/pest-plugin": true 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "require-dev": { 46 | "friendsofphp/php-cs-fixer": "^3.66", 47 | "leafs/alchemy": "^4.0" 48 | }, 49 | "scripts": { 50 | "alchemy": "./vendor/bin/alchemy setup", 51 | "test": "./vendor/bin/alchemy setup --test", 52 | "lint": "./vendor/bin/alchemy setup --lint", 53 | "actions": "./vendor/bin/alchemy setup --actions" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CreateCommand.php: -------------------------------------------------------------------------------- 1 | 'leafs/db', 19 | 'Authentication' => 'leafs/auth', 20 | 'Session support' => 'leafs/session', 21 | 'Cookie support' => 'leafs/cookie', 22 | 'CSRF protection' => 'leafs/csrf', 23 | 'CORS support' => 'leafs/cors', 24 | 'Leaf Date' => 'leafs/date', 25 | 'Leaf Fetch' => 'leafs/fetch', 26 | ]; 27 | 28 | protected $projectName; 29 | protected $projectType; 30 | 31 | protected $signature = 'create 32 | {project-name? : The name of the project} 33 | {--basic? : Create a raw leaf project} 34 | {--api? : Create a new Leaf MVC project for APIs} 35 | {--mvc? : Create a new Leaf MVC project} 36 | {--docker? : Scaffold a docker environment} 37 | {--force? : Forces install even if the directory already exists}'; 38 | 39 | // {--custom? : Scaffold a personalized Leaf app} 40 | 41 | protected $description = 'Create a new Leaf project'; 42 | 43 | protected function handle(): int 44 | { 45 | $needsUpdate = Package::updateAvailable(); 46 | 47 | if ($needsUpdate) { 48 | $this->writeln('Update found, updating to the latest stable version...'); 49 | 50 | if (sprout()->run('php ' . dirname(__DIR__) . '/bin/leaf update')) { 51 | $this->writeln("Leaf CLI updated successfully, building your app...\n"); 52 | 53 | return sprout()->run('php ' . implode(' ', (array) $_SERVER['argv'])); 54 | } else { 55 | $this->writeln("❌ Leaf CLI update failed, please try again later\n"); 56 | $this->writeln("⚙️ Creating app with current version...\n"); 57 | } 58 | } 59 | 60 | $this->projectName = $this->argument('project-name'); 61 | $this->projectType = $this->option('basic') ? 'basic' : ($this->option('api') ? 'api' : ($this->option('mvc') ? 'mvc' : null)); 62 | 63 | $this->writeln("\033[32m 64 | _ __ _ _ ___ 65 | | | ___ __ _ / _| | || | / _ \ 66 | | | / _ \/ _` | |_ | || |_| | | | 67 | | |__| __/ (_| | _| |__ _| |_| | 68 | |_____\___|\__,_|_| |_|(_)___/ 69 | \033[0m\n"); 70 | 71 | $scaffoldOptions = sprout()->prompt([ 72 | [ 73 | 'type' => $this->projectName ? null : 'text', 74 | 'name' => 'name', 75 | 'message' => 'Project name', 76 | 'default' => 'my-leaf-project', 77 | 'validate' => function ($value) { 78 | if (empty($value)) { 79 | return 'Name cannot be empty'; 80 | } 81 | 82 | return true; 83 | } 84 | ], 85 | [ 86 | 'type' => $this->projectType ? null : 'select', 87 | 'name' => 'type', 88 | 'message' => 'Select a preset', 89 | 'default' => 0, 90 | 'choices' => [ 91 | ['title' => 'Basic Leaf app', 'value' => 'basic'], 92 | ['title' => 'Full-stack MVC app', 'value' => 'mvc'], 93 | ['title' => 'Leaf MVC API app', 'value' => 'api'], 94 | ], 95 | ], 96 | ]); 97 | 98 | $this->projectName ??= $scaffoldOptions['name']; 99 | $this->projectType ??= $scaffoldOptions['type']; 100 | 101 | $commands = []; 102 | $directory = path($this->projectName !== '.' ? getcwd() . '/' . $this->projectName : getcwd())->normalize(); 103 | 104 | if (!$this->option('force')) { 105 | $this->verifyApplicationDoesntExist($directory); 106 | } 107 | 108 | $this->writeln( 109 | "\n⚙️ Creating \"" 110 | . basename($directory) . '" in ./' 111 | . basename(dirname($directory)) . 112 | " using preset {$this->projectType}." 113 | ); 114 | 115 | if ($this->projectType === 'basic') { 116 | if ( 117 | !FS\Directory::copy(__DIR__ . '/themes/leaf3', $directory, [ 118 | 'recursive' => true, 119 | ]) 120 | ) { 121 | $this->writeln('❌ Failed to create project'); 122 | return 1; 123 | } 124 | 125 | $commands[] = "cd \"$directory\""; 126 | $commands[] = 'composer install --ansi'; 127 | } else { 128 | $commands[] = "composer create-project leafs/mvc \"$directory\" --ansi"; 129 | $commands[] = "cd \"$directory\""; 130 | } 131 | 132 | if ($this->option('no-ansi')) { 133 | $commands = array_map(function ($value) { 134 | return "$value --no-ansi"; 135 | }, $commands); 136 | } 137 | 138 | if ($this->option('quiet')) { 139 | $commands = array_map(function ($value) { 140 | return "$value --quiet"; 141 | }, $commands); 142 | } 143 | 144 | if (sprout()->process(implode(' && ', $commands))->setTimeout(null)->run() === 0) { 145 | if ($this->projectType === 'api') { 146 | if (\Leaf\FS\File::exists("$directory/vite.config.js")) { 147 | \Leaf\FS\File::delete("$directory/vite.config.js"); 148 | } 149 | 150 | if (\Leaf\FS\File::exists("$directory/package.json")) { 151 | \Leaf\FS\File::delete("$directory/package.json"); 152 | } 153 | 154 | \Leaf\FS\Directory::delete("$directory/app/views", ['recursive' => true]); 155 | \Leaf\FS\Directory::delete("$directory/app/routes", ['recursive' => true]); 156 | \Leaf\FS\Directory::delete("$directory/public/assets", ['recursive' => true]); 157 | 158 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/api/routes', "$directory/app/routes"); 159 | 160 | \Leaf\FS\File::write("$directory/leaf", function ($content) { 161 | return str_replace( 162 | 'Leaf\Core::loadConsole()', 163 | "Leaf\Core::mode('api');\nLeaf\Core::loadConsole()", 164 | $content 165 | ); 166 | }); 167 | } 168 | 169 | $this->writeln("\n🚀 Successfully created project " . basename($directory) . "\n"); 170 | 171 | $extraOptions = sprout()->prompt([ 172 | [ 173 | 'type' => $this->projectType !== 'basic' ? 'confirm' : null, 174 | 'name' => 'auth', 175 | 'message' => $this->projectType === 'api' ? 'Setup auth flow?' : 'Install application starter?', 176 | 'default' => true, 177 | ], 178 | [ 179 | 'type' => $this->projectType === 'mvc' ? 'select' : null, 180 | 'name' => 'view', 181 | 'message' => 'Select a view engine', 182 | 'default' => 0, 183 | 'choices' => [ 184 | [ 185 | 'title' => 'Default', 186 | 'value' => 'default', 187 | 'disabled' => function ($answers) { 188 | return $answers['auth'] ?? false; 189 | } 190 | ], 191 | ['title' => 'Blade + Tailwind', 'value' => 'tailwind'], 192 | ['title' => 'React JS', 'value' => 'react'], 193 | ['title' => 'Vue JS', 'value' => 'vue'], 194 | ['title' => 'Svelte', 'value' => 'svelte'], 195 | ], 196 | ], 197 | [ 198 | 'type' => 'confirm', 199 | 'name' => 'tests', 200 | 'message' => 'Set up tests?', 201 | 'default' => true, 202 | ], 203 | [ 204 | 'type' => 'confirm', 205 | 'name' => 'docker', 206 | 'message' => 'Set up docker?', 207 | 'default' => false, 208 | ], 209 | ]); 210 | 211 | $extraCommands = ["cd \"$directory\""]; 212 | 213 | if (isset($extraOptions['view']) && $extraOptions['view'] !== 'default') { 214 | $extraCommands[] = 'php leaf view:install --' . $extraOptions['view']; 215 | } 216 | 217 | if ($extraOptions['auth'] ?? false) { 218 | $extraCommands[] = "php leaf scaffold:auth"; 219 | } 220 | 221 | if ($extraOptions['tests'] ?? false) { 222 | $extraCommands[] = 'composer require --dev --ansi leafs/alchemy && ./vendor/bin/alchemy install --ansi'; 223 | } 224 | 225 | if ($this->projectType === 'api') { 226 | $extraCommands[] = 'composer require leafs/cors --ansi'; 227 | } 228 | 229 | $this->write("\n"); 230 | 231 | if ($extraOptions['docker']) { 232 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/docker', $directory, [ 233 | 'recursive' => true, 234 | ]); 235 | } 236 | 237 | if ($this->projectType !== 'basic') { 238 | \Leaf\FS\File::write("$directory/.env", function ($content) use ($directory) { 239 | return str_replace( 240 | ['LEAF_DB_NAME', 'LEAF_DB_USERNAME'], 241 | [str_replace('-', '_', basename($directory)), 'root'], 242 | $content 243 | ); 244 | }); 245 | } 246 | 247 | if (sprout()->process(implode(' && ', $extraCommands))->setTimeout(null)->run() === 0 || count($extraCommands) === 1) { 248 | $this->writeln("\n🚀 Application scaffolded successfully"); 249 | $this->writeln('👉 Get started with the following commands:'); 250 | $this->writeln("\n cd " . basename($directory)); 251 | $this->writeln(' leaf serve'); 252 | 253 | if ($extraOptions['tests']) { 254 | $this->writeln("\n👉 You can run tests with:"); 255 | $this->writeln("\n leaf run test"); 256 | } 257 | 258 | $this->writeln("\n🍁 How fast can you ship?"); 259 | } else { 260 | $this->writeln("\n❌ Could not scaffold extra options for " . basename($directory)); 261 | $this->writeln('👉 Get started with the following commands:'); 262 | $this->writeln("\n cd " . basename($directory)); 263 | $this->writeln(' leaf serve'); 264 | 265 | $this->writeln("\n🍁 Happy gardening!"); 266 | } 267 | } 268 | 269 | return 0; 270 | } 271 | 272 | /** 273 | * Verify that the application does not already exist. 274 | * 275 | * @param string $directory 276 | * @return void 277 | */ 278 | protected function verifyApplicationDoesntExist(string $directory) 279 | { 280 | if ((is_dir($directory) || is_file($directory)) && $directory != getcwd()) { 281 | throw new RuntimeException('Application already exists!'); 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/InstallCommand.php: -------------------------------------------------------------------------------- 1 | argument('packages'); 18 | $parsedPackages = []; 19 | 20 | if (count($packages)) { 21 | foreach ($packages as $package) { 22 | if (strpos($package, '/') == false) { 23 | $package = "leafs/$package"; 24 | } 25 | 26 | $package = str_replace('@', ':', $package); 27 | $package = ($this->option('dev') === true) ? "$package --dev" : $package; 28 | 29 | $parsedPackages[] = $package; 30 | } 31 | 32 | if (!sprout()->composer()->install(implode(' ', $parsedPackages) . ' --ansi')->isSuccessful()) { 33 | return 1; 34 | } 35 | 36 | return 0; 37 | } 38 | 39 | return (int) sprout()->composer()->install()->isSuccessful(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/InteractCommand.php: -------------------------------------------------------------------------------- 1 | writeln('Leaf interactive shell activated'); 18 | 19 | if (file_exists('vendor/autoload.php')) { 20 | require 'vendor/autoload.php'; 21 | } 22 | 23 | if (file_exists('index.php') && !file_exists('leaf')) { 24 | require 'index.php'; 25 | } 26 | 27 | if (!file_exists('vendor/autoload.php') && !file_exists('Config/bootstrap.php') && (file_exists('index.php') && file_exists('leaf'))) { 28 | $this->writeln('Required files not found, starting shell running in retard mode...'); 29 | } 30 | 31 | return (new Shell())->run(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RunCommand.php: -------------------------------------------------------------------------------- 1 | composer()->json()) { 18 | $this->writeln('No composer.json found in the current directory.'); 19 | return 1; 20 | } 21 | 22 | return (int) sprout() 23 | ->composer() 24 | ->runScript($this->argument('script')) 25 | ->isSuccessful(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServeCommand.php: -------------------------------------------------------------------------------- 1 | isMVCApp()) { 21 | return (int) sprout()->run("php leaf serve --port={$this->option('port')} --ansi", null); 22 | } 23 | 24 | if (!sprout()->composer()->json()) { 25 | $this->writeln('No composer.json found in the current directory.'); 26 | return 1; 27 | } 28 | 29 | if (!sprout()->composer()->hasDependencies()) { 30 | $this->writeln('Installing dependencies...'); 31 | 32 | if (!sprout()->composer()->install()->isSuccessful()) { 33 | $this->writeln('❌ Failed to install dependencies.'); 34 | return 1; 35 | } 36 | } 37 | 38 | $port = $this->option('port'); 39 | $isDockerProject = file_exists(getcwd() . '/docker-compose.yml'); 40 | $useConcurrent = !$this->option('no-concurrent') && (file_exists(getcwd() . '/vite.config.js') && file_exists(getcwd() . '/package.json')); 41 | $serveCommand = !$useConcurrent ? "php -S localhost:$port" : "npx concurrently -c \"#3eaf7c,#bd34fe\" \"php -S localhost:$port\" \"npm run dev\" --names=server,vite --colors"; 42 | 43 | return sprout() 44 | ->process($isDockerProject ? 'docker compose up' : $serveCommand) 45 | ->setTimeout(null) 46 | ->run(); 47 | } 48 | 49 | protected function isMVCApp() 50 | { 51 | $directory = getcwd(); 52 | 53 | return is_dir("$directory/app/views") && file_exists("$directory/leaf") && is_dir("$directory/public"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/UICommand.php: -------------------------------------------------------------------------------- 1 | option('port'); 18 | 19 | $uiDirectory = __DIR__ . '/ui/dist'; 20 | $serveCommand = "cd $uiDirectory && php -S localhost:$port"; 21 | 22 | $process = sprout()->process($serveCommand); 23 | 24 | $this->writeln("CLI GUI started at http://localhost:$port"); 25 | 26 | return $process->run(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/UninstallCommand.php: -------------------------------------------------------------------------------- 1 | argument('packages'); 18 | $parsedPackages = []; 19 | 20 | if (!sprout()->composer()->json()) { 21 | $this->writeln('No composer.json found in the current directory.'); 22 | return 1; 23 | } 24 | 25 | foreach ($packages as $package) { 26 | if (strpos($package, '/') == false) { 27 | $package = "leafs/$package"; 28 | } 29 | 30 | $parsedPackages[] = $package; 31 | } 32 | 33 | if (!sprout()->composer()->remove(implode(' ', $parsedPackages) . ' --ansi')->isSuccessful()) { 34 | return 1; 35 | } 36 | 37 | $this->writeln('packages uninstalled successfully!'); 38 | 39 | return 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/UpdateCommand.php: -------------------------------------------------------------------------------- 1 | composer(true)->remove('leafs/cli --no-update --no-install --ansi')->isSuccessful()) { 18 | sleep(1); 19 | 20 | if (sprout()->composer(true)->install('leafs/cli --ansi')->isSuccessful()) { 21 | $this->writeln('Leaf CLI installed successfully!'); 22 | return 0; 23 | } 24 | } 25 | 26 | $this->writeln('Could not update CLI, please retry!'); 27 | 28 | return 1; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Utils/Core.php: -------------------------------------------------------------------------------- 1 | run(function ($type, $line) use ($output) { 25 | $output->write($line); 26 | }); 27 | 28 | return $process->isSuccessful(); 29 | } 30 | 31 | /** 32 | * Get the composer command for the environment. 33 | * @return string 34 | */ 35 | public static function findComposer(): string 36 | { 37 | $composerPath = getcwd() . '/composer.phar'; 38 | 39 | if (file_exists($composerPath)) { 40 | return '"' . PHP_BINARY . '" ' . $composerPath; 41 | } 42 | 43 | return 'composer'; 44 | } 45 | 46 | /** 47 | * Get the git command for the environment. 48 | * @return string 49 | */ 50 | public static function findGit(): string 51 | { 52 | $gitPath = getcwd() . '/git'; 53 | 54 | if (file_exists($gitPath)) { 55 | return $gitPath; 56 | } 57 | 58 | return 'git'; 59 | } 60 | 61 | /** 62 | * Get the node command for the environment. 63 | * @return string 64 | */ 65 | public static function findNodeJS(): string 66 | { 67 | $nodePath = getcwd() . '/node'; 68 | 69 | if (file_exists($nodePath)) { 70 | return $nodePath; 71 | } 72 | 73 | return 'node'; 74 | } 75 | 76 | /** 77 | * Get the node command for the environment. 78 | * @return string 79 | */ 80 | public static function findNpm($packageManager = 'npm'): string 81 | { 82 | $npmPath = getcwd() . "/$packageManager"; 83 | 84 | if (file_exists($npmPath)) { 85 | return $npmPath; 86 | } 87 | 88 | return $packageManager; 89 | } 90 | 91 | /** 92 | * Get the leaf CLI bin. 93 | * @return string 94 | */ 95 | public static function findLeaf(): string 96 | { 97 | $leafPath = __DIR__ . '/../../bin/leaf'; 98 | 99 | if (file_exists($leafPath)) { 100 | return '"' . PHP_BINARY . '" ' . $leafPath; 101 | } 102 | 103 | return 'leaf'; 104 | } 105 | 106 | /** 107 | * Get the leaf watcher bin. 108 | * @return string 109 | */ 110 | public static function findWatcher(): string 111 | { 112 | $watcherPath = getcwd() . '/watcher/bin/watcher.js'; 113 | 114 | if (file_exists($watcherPath)) { 115 | return $watcherPath; 116 | } 117 | 118 | return 'leaf-watcher'; 119 | } 120 | 121 | /** 122 | * Check if a system command exists 123 | * @return bool 124 | */ 125 | public static function commandExists(string $cmd) 126 | { 127 | return !empty(shell_exec(sprintf('which %s', escapeshellarg($cmd)))); 128 | } 129 | 130 | /** 131 | * Check if a project is a blade project 132 | */ 133 | public static function isBladeProject($directory = null) 134 | { 135 | $isBladeProject = false; 136 | $directory = $directory ?? getcwd(); 137 | 138 | if (file_exists("$directory/config/view.php")) { 139 | $viewConfig = require "$directory/config/view.php"; 140 | $isBladeProject = strpos(strtolower($viewConfig['viewEngine'] ?? $viewConfig['view_engine'] ?? ''), 'blade') !== false; 141 | } elseif (file_exists("$directory/composer.lock")) { 142 | $composerLock = json_decode(file_get_contents("$directory/composer.lock"), true); 143 | $packages = $composerLock['packages'] ?? []; 144 | foreach ($packages as $package) { 145 | if ($package['name'] === 'leafs/blade') { 146 | $isBladeProject = true; 147 | 148 | break; 149 | } 150 | } 151 | } 152 | 153 | return $isBladeProject; 154 | } 155 | 156 | /** 157 | * Check if a project is an MVC project 158 | */ 159 | public static function isMVCProject($directory = null) 160 | { 161 | $directory = $directory ?? getcwd(); 162 | 163 | return is_dir("$directory/app/views") && file_exists("$directory/leaf") && is_dir("$directory/public"); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Utils/Package.php: -------------------------------------------------------------------------------- 1 | version; 32 | } 33 | 34 | /** 35 | * Find latest stable version 36 | */ 37 | public static function ltsInfo() 38 | { 39 | $data = file_get_contents('https://repo.packagist.org/p2/leafs/cli.json'); 40 | 41 | if (!$data) { 42 | return static::info(); 43 | } 44 | 45 | $package = json_decode($data); 46 | 47 | return $package->packages->{'leafs/cli'}[0]; 48 | } 49 | 50 | /** 51 | * Find latest stable version 52 | */ 53 | public static function ltsVersion() 54 | { 55 | $package = static::ltsInfo(); 56 | 57 | return $package->version; 58 | } 59 | 60 | /** 61 | * Check if there is an update available 62 | */ 63 | public static function updateAvailable() 64 | { 65 | $currentVersion = static::version(); 66 | $latestVersion = static::ltsVersion(); 67 | 68 | if ($currentVersion > $latestVersion) { 69 | return false; 70 | } 71 | 72 | return ($currentVersion !== $latestVersion); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ViewBuildCommand.php: -------------------------------------------------------------------------------- 1 | npm()->json()) { 18 | $this->writeln('No package.json found in the current directory.'); 19 | return 1; 20 | } 21 | 22 | if (!sprout()->npm()->hasDependencies()) { 23 | $this->writeln('Installing dependencies...'); 24 | 25 | if (!sprout()->npm($this->option('pm'))->install()->isSuccessful()) { 26 | $this->writeln('❌ Failed to install dependencies.'); 27 | return 1; 28 | } 29 | } 30 | 31 | $this->writeln('Building assets...'); 32 | 33 | return (int) sprout() 34 | ->npm($this->option('pm')) 35 | ->runScript('build') 36 | ->isSuccessful(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ViewInstallCommand.php: -------------------------------------------------------------------------------- 1 | option('blade')) { 26 | return $this->installBlade(); 27 | } 28 | 29 | if ($this->option('bare-ui')) { 30 | return $this->installBareUi(); 31 | } 32 | 33 | if ($this->option('react')) { 34 | return $this->installReact(); 35 | } 36 | 37 | if ($this->option('svelte')) { 38 | return $this->installSvelte(); 39 | } 40 | 41 | if ($this->option('tailwind')) { 42 | return $this->installTailwind(); 43 | } 44 | 45 | if ($this->option('vite')) { 46 | return $this->installVite(); 47 | } 48 | 49 | if ($this->option('vue')) { 50 | return $this->installVue(); 51 | } 52 | 53 | // $selections = sprout()->prompt([ 54 | // [ 55 | // 'type' => 'select', 56 | // 'name' => 'type', 57 | // 'message' => 'What do you want to install?', 58 | // 'default' => 0, 59 | // 'choices' => [ 60 | // ['title' => 'React', 'value' => 'react'], 61 | // ['title' => 'Svelte', 'value' => 'svelte'], 62 | // ['title' => 'Vue', 'value' => 'vue'], 63 | // ['title' => 'Tailwind CSS', 'value' => 'tailwind'], 64 | // ['title' => 'Vite', 'value' => 'vite'], 65 | // ['title' => 'Blade', 'value' => 'blade'], 66 | // ['title' => 'Bare UI', 'value' => 'bare-ui'], 67 | // ], 68 | // ] 69 | // ]); 70 | 71 | // $this->projectType ??= $selections['type']; 72 | 73 | return 1; 74 | } 75 | 76 | /** 77 | * Install blade 78 | */ 79 | protected function installBlade() 80 | { 81 | $directory = getcwd(); 82 | $isMVCApp = $this->isMVCApp(); 83 | 84 | if ($isMVCApp) { 85 | $this->writeln("❌ Blade is already installed in this project"); 86 | return 1; 87 | } 88 | 89 | $this->writeln("📦 Installing blade...\n"); 90 | 91 | if ( 92 | sprout()->composer()->install('leafs/blade')->run() || !\Leaf\FS\Directory::copy(__DIR__ . '/themes/blade --ansi', $directory !== 0, [ 93 | 'recursive' => true, 94 | ]) 95 | ) { 96 | $this->writeln('❌ Failed to install blade'); 97 | return 1; 98 | } 99 | 100 | $this->writeln("\n🎉 Blade setup successfully. Include the setup/_blade.php file to get started"); 101 | $this->writeln("👉 Read the blade docs to create your first template.\n"); 102 | 103 | return 0; 104 | } 105 | 106 | /** 107 | * Install bare ui 108 | */ 109 | protected function installBareUi() 110 | { 111 | $directory = getcwd(); 112 | $isMVCApp = $this->isMVCApp(); 113 | 114 | if ($isMVCApp) { 115 | $this->writeln("❌ Blade detected, skipping..."); 116 | return 1; 117 | } 118 | 119 | $this->writeln("📦 Installing bare-ui...\n"); 120 | 121 | if ( 122 | sprout()->composer()->install('leafs/bareui')->run() || !\Leaf\FS\Directory::copy(__DIR__ . '/themes/bareui --ansi', $directory !== 0, [ 123 | 'recursive' => true, 124 | ]) 125 | ) { 126 | $this->writeln('❌ Failed to install bareui'); 127 | return 1; 128 | } 129 | 130 | $this->writeln("\n🎉 Bare UI setup successfully. Include the setup/_bareui.php file to get started"); 131 | $this->writeln("👉 Read the blade docs to create your first template.\n"); 132 | 133 | return 0; 134 | } 135 | 136 | /** 137 | * Install react 138 | */ 139 | protected function installReact() 140 | { 141 | $directory = getcwd(); 142 | 143 | if ($this->isMVCApp()) { 144 | return (int) sprout()->run("php $directory/leaf view:install --react --ansi"); 145 | } 146 | 147 | $this->writeln("📦 Installing react...\n"); 148 | 149 | if (sprout()->npm($this->option('pm'))->install('@leafphp/vite-plugin @vitejs/plugin-react @inertiajs/react react@18 react-dom@18')->run() !== 0) { 150 | $this->writeln('❌ Failed to install react'); 151 | return 1; 152 | } 153 | 154 | $this->writeln("\n✅ React installed successfully"); 155 | $this->writeln("🧱 Setting up Leaf React server bridge...\n"); 156 | 157 | if (sprout()->composer()->install('leafs/inertia leafs/blade leafs/vite --ansi')->run() !== 0) { 158 | $this->writeln('❌ Failed to setup Leaf React server bridge'); 159 | return 1; 160 | } 161 | 162 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/react', $directory, [ 163 | 'recursive' => true, 164 | ]); 165 | 166 | $package = json_decode(file_get_contents("$directory/package.json"), true); 167 | $package['type'] = 'module'; 168 | $package['scripts']['dev'] = 'vite'; 169 | $package['scripts']['build'] = 'vite build'; 170 | file_put_contents("$directory/package.json", json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 171 | 172 | if (\Leaf\FS\File::exists("$directory/vite.config.js")) { 173 | \Leaf\FS\File::write("$directory/vite.config.js", function ($content) { 174 | if (strpos($content, "@vitejs/plugin-react") === false) { 175 | $content = str_replace( 176 | ["import leaf from '@leafphp/vite-plugin';", 'import leaf from "@leafphp/vite-plugin";'], 177 | "import leaf from '@leafphp/vite-plugin';\nimport react from '@vitejs/plugin-react';", 178 | $content 179 | ); 180 | } 181 | 182 | if (strpos($content, "react()") === false) { 183 | $content = str_replace("leaf({", "react(),\nleaf({", $content); 184 | } 185 | 186 | return $content; 187 | }); 188 | } 189 | 190 | $this->writeln("\n💙 React setup successfully"); 191 | $this->writeln("👉 Get started with the following commands:\n"); 192 | $this->writeln(' leaf serve - start dev server'); 193 | $this->writeln(" leaf view:build - build for production\n"); 194 | 195 | return 0; 196 | } 197 | 198 | /** 199 | * Install svelte 200 | */ 201 | protected function installSvelte() 202 | { 203 | $directory = getcwd(); 204 | 205 | if ($this->isMVCApp()) { 206 | return (int) sprout()->run("php $directory/leaf view:install --svelte --ansi"); 207 | } 208 | 209 | $this->writeln("📦 Installing svelte...\n"); 210 | 211 | if (sprout()->npm($this->option('pm'))->install('@leafphp/vite-plugin svelte @sveltejs/vite-plugin-svelte @inertiajs/svelte')->run() !== 0) { 212 | $this->writeln('❌ Failed to install svelte'); 213 | return 1; 214 | } 215 | 216 | $this->writeln("\n✅ Svelte installed successfully"); 217 | $this->writeln("🧱 Setting up Leaf Svelte server bridge...\n"); 218 | 219 | if (sprout()->composer()->install('leafs/inertia leafs/blade leafs/vite --ansi')->run() !== 0) { 220 | $this->writeln('❌ Failed to setup Leaf svelte server bridge'); 221 | return 1; 222 | } 223 | 224 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/svelte', $directory, [ 225 | 'recursive' => true, 226 | ]); 227 | 228 | $package = json_decode(file_get_contents("$directory/package.json"), true); 229 | $package['type'] = 'module'; 230 | $package['scripts']['dev'] = 'vite'; 231 | $package['scripts']['build'] = 'vite build'; 232 | file_put_contents("$directory/package.json", json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 233 | 234 | if (\Leaf\FS\File::exists("$directory/vite.config.js")) { 235 | \Leaf\FS\File::write("$directory/vite.config.js", function ($content) { 236 | if (strpos($content, "@sveltejs/vite-plugin-svelte") === false) { 237 | $content = str_replace( 238 | ["import leaf from '@leafphp/vite-plugin';", 'import leaf from "@leafphp/vite-plugin";'], 239 | "import leaf from '@leafphp/vite-plugin';\nimport { svelte } from '@sveltejs/vite-plugin-svelte'", 240 | $content 241 | ); 242 | } 243 | 244 | if (strpos($content, "svelte()") === false) { 245 | $content = str_replace("leaf({", "svelte(),\nleaf({", $content); 246 | } 247 | 248 | return $content; 249 | }); 250 | } 251 | 252 | $this->writeln("\n🧡 Svelte setup successfully"); 253 | $this->writeln("👉 Get started with the following commands:\n"); 254 | $this->writeln(' leaf serve - start dev server'); 255 | $this->writeln(" leaf view:build - build for production\n"); 256 | 257 | return 0; 258 | } 259 | 260 | /** 261 | * Install tailwind 262 | */ 263 | protected function installTailwind() 264 | { 265 | $directory = getcwd(); 266 | 267 | if ($this->isMVCApp()) { 268 | return (int) sprout()->run("php $directory/leaf view:install --svelte --ansi"); 269 | } 270 | 271 | $this->writeln("📦 Installing tailwind...\n"); 272 | 273 | if (sprout()->npm($this->option('pm'))->install('@leafphp/vite-plugin tailwindcss @tailwindcss/vite')->run() !== 0) { 274 | $this->writeln('❌ Failed to install tailwind'); 275 | return 1; 276 | } 277 | 278 | $this->writeln("\n✅ Tailwind installed successfully"); 279 | $this->writeln("🧱 Setting up Leaf Tailwind server bridge...\n"); 280 | 281 | if (sprout()->composer()->install('leafs/vite --ansi')->run() !== 0) { 282 | $this->writeln('❌ Failed to setup Leaf Tailwind server bridge'); 283 | return 1; 284 | } 285 | 286 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/tailwind', $directory, [ 287 | 'recursive' => true, 288 | ]); 289 | 290 | $package = json_decode(file_get_contents("$directory/package.json"), true); 291 | $package['type'] = 'module'; 292 | $package['scripts']['dev'] = 'vite'; 293 | $package['scripts']['build'] = 'vite build'; 294 | file_put_contents("$directory/package.json", json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 295 | 296 | if (file_exists("$directory/views/js/app.js")) { 297 | $jsApp = file_get_contents("$directory/views/js/app.js"); 298 | 299 | if (strpos($jsApp, "import '../css/app.css';") === false) { 300 | \Leaf\FS\File::write("$directory/views/js/app.js", function ($content) { 301 | return "import '../css/app.css';\n$content"; 302 | }); 303 | } 304 | } elseif (file_exists("$directory/views/js/app.jsx")) { 305 | $jsApp = file_get_contents("$directory/views/js/app.jsx"); 306 | 307 | if (strpos($jsApp, "import '../css/app.css';") === false) { 308 | \Leaf\FS\File::write("$directory/views/js/app.jsx", function ($content) { 309 | return "import '../css/app.css';\n$content"; 310 | }); 311 | } 312 | } 313 | 314 | if (file_exists("$directory/views/css/app.css")) { 315 | \Leaf\FS\File::write("$directory/views/css/app.css", function ($content) { 316 | if (strpos($content, '@import "tailwindcss";') === false) { 317 | return "@import \"tailwindcss\";\n@source \"../\";\n\n$content"; 318 | } 319 | 320 | return $content; 321 | }); 322 | } 323 | 324 | $this->writeln("\n🩵 Tailwind CSS setup successfully"); 325 | $this->writeln("👉 Get started with the following commands:\n"); 326 | $this->writeln(' leaf serve - start dev server'); 327 | $this->writeln(" leaf view:build - build for production\n"); 328 | 329 | return 0; 330 | } 331 | 332 | /** 333 | * Install vite 334 | */ 335 | protected function installVite() 336 | { 337 | $directory = getcwd(); 338 | 339 | if ($this->isMVCApp()) { 340 | return (int) sprout()->run("php $directory/leaf view:install --svelte --ansi"); 341 | } 342 | 343 | $this->writeln("📦 Installing vite...\n"); 344 | 345 | if (sprout()->npm($this->option('pm'))->install('@leafphp/vite-plugin vite')->run() !== 0) { 346 | $this->writeln('❌ Failed to install vite'); 347 | return 1; 348 | } 349 | 350 | $this->writeln("\n✅ Vite installed successfully"); 351 | $this->writeln("🧱 Setting up Leaf Vite server bridge...\n"); 352 | 353 | if (sprout()->composer()->install('leafs/vite --ansi')->run() !== 0) { 354 | $this->writeln('❌ Failed to setup Leaf Vite server bridge'); 355 | return 1; 356 | } 357 | 358 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/vite', $directory, [ 359 | 'recursive' => true, 360 | ]); 361 | 362 | $this->writeln("\n💜 Vite setup successfully"); 363 | $this->writeln("👉 Get started with the following commands:\n"); 364 | $this->writeln(' leaf serve - start dev server'); 365 | $this->writeln(" leaf view:build - build for production\n"); 366 | 367 | return 0; 368 | } 369 | 370 | /** 371 | * Install vue 372 | */ 373 | protected function installVue() 374 | { 375 | $directory = getcwd(); 376 | 377 | if ($this->isMVCApp()) { 378 | return (int) sprout()->run("php $directory/leaf view:install --svelte --ansi"); 379 | } 380 | 381 | $this->writeln("📦 Installing Vue...\n"); 382 | 383 | if (sprout()->npm($this->option('pm'))->install('@leafphp/vite-plugin @vitejs/plugin-vue @inertiajs/vue3@^1.0 vue')->run() !== 0) { 384 | $this->writeln('❌ Failed to install Vue'); 385 | return 1; 386 | } 387 | 388 | $this->writeln("\n✅ Vue installed successfully"); 389 | $this->writeln("🧱 Setting up Leaf Vue server bridge...\n"); 390 | 391 | if (sprout()->composer()->install('leafs/inertia leafs/blade leafs/vite --ansi')->run() !== 0) { 392 | $this->writeln('❌ Failed to setup Leaf Vue server bridge'); 393 | return 1; 394 | } 395 | 396 | \Leaf\FS\Directory::copy(__DIR__ . '/themes/tailwind', $directory, [ 397 | 'recursive' => true, 398 | ]); 399 | 400 | $package = json_decode(file_get_contents("$directory/package.json"), true); 401 | $package['type'] = 'module'; 402 | $package['scripts']['dev'] = 'vite'; 403 | $package['scripts']['build'] = 'vite build'; 404 | file_put_contents("$directory/package.json", json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 405 | 406 | if (\Leaf\FS\File::exists("$directory/vite.config.js")) { 407 | \Leaf\FS\File::write("$directory/vite.config.js", function ($content) { 408 | if (strpos($content, "@vitejs/plugin-vue") === false) { 409 | $content = str_replace( 410 | ["import leaf from '@leafphp/vite-plugin';", 'import leaf from "@leafphp/vite-plugin";'], 411 | "import leaf from '@leafphp/vite-plugin';\nimport vue from '@vitejs/plugin-vue';", 412 | $content 413 | ); 414 | } 415 | 416 | if (strpos($content, "vue(") === false) { 417 | $content = str_replace("leaf({", "vue({ 418 | template: { 419 | transformAssetUrls: { 420 | base: null, 421 | includeAbsolute: false, 422 | }, 423 | }, 424 | }),\nleaf({", $content); 425 | } 426 | 427 | return $content; 428 | }); 429 | } 430 | 431 | $this->writeln("\n💚 Vue setup successfully"); 432 | $this->writeln("👉 Get started with the following commands:\n"); 433 | $this->writeln(' leaf serve - start dev server'); 434 | $this->writeln(" leaf view:build - build for production\n"); 435 | 436 | return 0; 437 | } 438 | 439 | // ------------------------ utils ------------------------ // 440 | protected function isMVCApp() 441 | { 442 | $directory = getcwd(); 443 | 444 | return is_dir("$directory/app/views") && file_exists("$directory/leaf") && is_dir("$directory/public"); 445 | } 446 | 447 | protected function isBladeProject($directory = null) 448 | { 449 | $isBladeProject = false; 450 | $directory ??= getcwd(); 451 | 452 | if (file_exists("$directory/composer.lock")) { 453 | $composerLock = json_decode(file_get_contents("$directory/composer.lock"), true); 454 | $packages = $composerLock['packages'] ?? []; 455 | 456 | foreach ($packages as $package) { 457 | if ($package['name'] === 'leafs/blade') { 458 | $isBladeProject = true; 459 | break; 460 | } 461 | } 462 | } 463 | 464 | return $isBladeProject; 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /src/themes/api/routes/_app.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 4 | response()->json(['message' => 'Congrats!! You\'re on Leaf MVC']); 5 | }); 6 | -------------------------------------------------------------------------------- /src/themes/api/routes/index.php: -------------------------------------------------------------------------------- 1 | set404(). Whatever function 10 | | you set will be called when a 404 error is encountered 11 | | 12 | */ 13 | app()->set404(function () { 14 | response()->json('Resource not found', 404, true); 15 | }); 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Set up 500 handler 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Leaf provides a default 500 page, but you can create your own error 23 | | 500 handler by calling the setErrorHandler() method. The function 24 | | you set will be called when a 500 error is encountered 25 | | 26 | */ 27 | app()->setErrorHandler(function () { 28 | response()->json('An error occured, our team has been notified', 500, true); 29 | }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Set middleware for all routes 34 | |-------------------------------------------------------------------------- 35 | | 36 | | You can use app()->use() to load middleware for all 37 | | routes in your application. 38 | | 39 | */ 40 | // app()->use(ExampleMiddleware::class); 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Your application routes 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Leaf MVC automatically loads all files in the routes folder that 48 | | start with "_". We call these files route partials. An example 49 | | partial has been created for you. 50 | | 51 | | If you want to manually load routes, you can 52 | | create a file that doesn't start with "_" and manually require 53 | | it here like so: 54 | | 55 | */ 56 | // require __DIR__ . '/custom-route.php'; 57 | -------------------------------------------------------------------------------- /src/themes/bareui/setup/_bareui.php: -------------------------------------------------------------------------------- 1 | template()->config('path', './views'); 4 | -------------------------------------------------------------------------------- /src/themes/bareui/views/index.view.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | Hello World 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/themes/blade/setup/_blade.php: -------------------------------------------------------------------------------- 1 | attachView(Leaf\Blade::class); 4 | app()->blade()->configure([ 5 | 'views' => 'views', 6 | 'cache' => 'cache' 7 | ]); 8 | -------------------------------------------------------------------------------- /src/themes/blade/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ _env('APP_NAME', 'My Leaf MVC App') }} 8 | 9 | 10 | 11 | {{-- @vite('css/app.css') --}} 12 | 13 | 15 | 16 | 17 | @alpine 18 | 19 | 20 | 22 |
23 |
24 | 25 |
26 | 154 |
155 | 156 | 210 |
211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /src/themes/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | application: 4 | build: ./docker 5 | image: leafphp/docker 6 | ports: 7 | - '8080:80' 8 | volumes: 9 | - .:/var/www 10 | -------------------------------------------------------------------------------- /src/themes/docker/docker/000-default.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerAdmin webmaster@localhost 3 | DocumentRoot /var/www 4 | 5 | 6 | Options FollowSymLinks 7 | AllowOverride All 8 | Require all granted 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/themes/docker/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-apache 2 | 3 | COPY 000-default.conf /etc/apache2/sites-available/000-default.conf 4 | 5 | RUN a2enmod rewrite 6 | 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | libzip-dev \ 9 | wget \ 10 | git \ 11 | unzip 12 | 13 | RUN docker-php-ext-install zip pdo pdo_mysql 14 | 15 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 16 | 17 | RUN composer global require leafs/cli 18 | 19 | RUN ln -s /root/.composer/vendor/bin/leaf /usr/local/bin/leaf 20 | 21 | COPY ./php.ini /usr/local/etc/php/php.ini 22 | 23 | RUN apt-get purge -y g++ \ 24 | && apt-get autoremove -y \ 25 | && rm -rf /var/lib/apt/lists/* \ 26 | && rm -rf /tmp/* 27 | 28 | WORKDIR /var/www 29 | 30 | RUN chown -R www-data:www-data /var/www 31 | 32 | CMD ["apache2-foreground"] 33 | -------------------------------------------------------------------------------- /src/themes/docker/docker/php.ini: -------------------------------------------------------------------------------- 1 | ; General 2 | upload_max_filesize = 200M 3 | post_max_size = 220M 4 | -------------------------------------------------------------------------------- /src/themes/inertia/root/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import leaf from '@leafphp/vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | leaf({ 8 | input: ['app/views/js/app.jsx'], 9 | refresh: true, 10 | }), 11 | react(), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /src/themes/inertia/views/bare-ui/_inertia.view.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | dispatch($page); 17 | } 18 | 19 | if ($__inertiaSsrResponse) { 20 | echo $__inertiaSsrResponse->head; 21 | } 22 | ?> 23 | 24 | 25 | 26 | dispatch($page); 30 | } 31 | 32 | if ($__inertiaSsrResponse) { 33 | echo $__inertiaSsrResponse->body; 34 | } else { 35 | echo '
'; 36 | } 37 | ?> 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/themes/inertia/views/blade/_inertia.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | {{-- react --}} 9 | 10 | 11 | {{-- vue --}} 12 | 13 | dispatch($page); 17 | } 18 | 19 | if ($__inertiaSsrResponse) { 20 | echo $__inertiaSsrResponse->head; 21 | } 22 | ?> 23 | 24 | 25 | 26 | dispatch($page); 30 | } 31 | 32 | if ($__inertiaSsrResponse) { 33 | echo $__inertiaSsrResponse->body; 34 | } else { 35 | echo '
'; 36 | } 37 | ?> 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/themes/leaf3/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-d 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule . index.php [L] -------------------------------------------------------------------------------- /src/themes/leaf3/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "leafs/leaf": "v4.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/themes/leaf3/index.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 6 | response()->page('./welcome.html'); 7 | }); 8 | 9 | app()->run(); 10 | -------------------------------------------------------------------------------- /src/themes/mvc/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | application: 4 | build: ./docker 5 | image: leafphp/docker 6 | ports: 7 | - '8080:80' 8 | volumes: 9 | - .:/var/www 10 | -------------------------------------------------------------------------------- /src/themes/mvc/docker/docker/000-default.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerAdmin webmaster@localhost 3 | ServerName localhost 4 | DocumentRoot /var/www/public 5 | 6 | 7 | Options FollowSymLinks 8 | AllowOverride All 9 | Require all granted 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/themes/mvc/docker/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-apache 2 | 3 | COPY 000-default.conf /etc/apache2/sites-available/000-default.conf 4 | 5 | RUN a2enmod rewrite 6 | 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | libzip-dev \ 9 | wget \ 10 | git \ 11 | unzip 12 | 13 | RUN docker-php-ext-install zip pdo pdo_mysql 14 | 15 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 16 | 17 | RUN composer global require leafs/cli 18 | 19 | RUN ln -s /root/.composer/vendor/bin/leaf /usr/local/bin/leaf 20 | 21 | COPY ./php.ini /usr/local/etc/php/php.ini 22 | 23 | RUN apt-get purge -y g++ \ 24 | && apt-get autoremove -y \ 25 | && rm -rf /var/lib/apt/lists/* \ 26 | && rm -rf /tmp/* 27 | 28 | WORKDIR /var/www 29 | 30 | RUN chown -R www-data:www-data /var/www 31 | 32 | CMD ["apache2-foreground"] 33 | -------------------------------------------------------------------------------- /src/themes/mvc/docker/docker/php.ini: -------------------------------------------------------------------------------- 1 | ; General 2 | upload_max_filesize = 200M 3 | post_max_size = 220M 4 | -------------------------------------------------------------------------------- /src/themes/react/routes/_frontend.php: -------------------------------------------------------------------------------- 1 | get('/hello', function () { 4 | inertia('Hello'); 5 | }); 6 | -------------------------------------------------------------------------------- /src/themes/react/views/_inertia.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | dispatch($page); 14 | } 15 | 16 | if ($__inertiaSsrResponse) { 17 | echo $__inertiaSsrResponse->head; 18 | } 19 | ?> 20 | 21 | 22 | 23 | dispatch($page); 27 | } 28 | 29 | if ($__inertiaSsrResponse) { 30 | echo $__inertiaSsrResponse->body; 31 | } else { 32 | echo '
'; 33 | } 34 | ?> 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/themes/react/views/js/Pages/Hello.jsx: -------------------------------------------------------------------------------- 1 | import { Link, Head } from '@inertiajs/react'; 2 | 3 | const Hello = ({ auth }) => { 4 | return ( 5 | <> 6 | 7 |
8 |

Hello World from React

9 |

Current user: {auth?.user?.name ?? 'No auth is active'}

10 | Go to Login 11 |
12 | 13 | ); 14 | }; 15 | 16 | export default Hello; 17 | -------------------------------------------------------------------------------- /src/themes/react/views/js/Pages/Login.jsx: -------------------------------------------------------------------------------- 1 | const Hello = ({ auth, welcome }) => { 2 | return ( 3 |
4 |

Login

5 |

{welcome}

6 |
7 | ) 8 | } 9 | 10 | export default Hello; 11 | -------------------------------------------------------------------------------- /src/themes/react/views/js/app.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { createInertiaApp } from '@inertiajs/react'; 3 | import { resolvePageComponent } from '@leafphp/vite-plugin/inertia-helpers'; 4 | 5 | const appName = import.meta.env.VITE_APP_NAME || 'Leaf PHP'; 6 | 7 | createInertiaApp({ 8 | title: (title) => `${title} - ${appName}`, 9 | resolve: (name) => 10 | resolvePageComponent( 11 | `./Pages/${name}.jsx`, 12 | import.meta.glob('./Pages/**/*.jsx') 13 | ), 14 | setup({ el, App, props }) { 15 | createRoot(el).render(); 16 | }, 17 | progress: { 18 | color: '#4B5563', 19 | }, 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /src/themes/react/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import leaf from '@leafphp/vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | leaf({ 8 | hotFile: 'hot', 9 | input: ['app/views/js/app.jsx'], 10 | refresh: true, 11 | }), 12 | react(), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /src/themes/svelte/routes/_frontend.php: -------------------------------------------------------------------------------- 1 | get('/hello', function () { 4 | inertia('Hello'); 5 | }); 6 | -------------------------------------------------------------------------------- /src/themes/svelte/views/_inertia.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | dispatch($page); 13 | } 14 | 15 | if ($__inertiaSsrResponse) { 16 | echo $__inertiaSsrResponse->head; 17 | } 18 | ?> 19 | 20 | 21 | 22 | dispatch($page); 26 | } 27 | 28 | if ($__inertiaSsrResponse) { 29 | echo $__inertiaSsrResponse->body; 30 | } else { 31 | echo '
'; 32 | } 33 | ?> 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/themes/svelte/views/js/Pages/Hello.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |

Hello World!

13 |

Current user: {auth?.user?.name ?? "No auth is active"}

14 |

{count}

15 | 16 |
17 | -------------------------------------------------------------------------------- /src/themes/svelte/views/js/app.js: -------------------------------------------------------------------------------- 1 | import { createInertiaApp } from "@inertiajs/svelte"; 2 | import { resolvePageComponent } from "@leafphp/vite-plugin/inertia-helpers"; 3 | 4 | const appName = import.meta.env.VITE_APP_NAME || "Leaf PHP"; 5 | 6 | createInertiaApp({ 7 | title: (title) => `${title} - ${appName}`, 8 | resolve: (name) => 9 | resolvePageComponent(`./Pages/${name}.svelte`, 10 | import.meta.glob('./Pages/**/*.svelte', { eager: true }) 11 | ), 12 | //or with persistent layouts 13 | // {// setting the default page layout 14 | // const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) 15 | // let page = pages[`./Pages/${name}.svelte`] 16 | // return { default: page.default, layout: page.layout || Layout } 17 | // }, 18 | setup({ el, App, props, plugin }) { 19 | new App({ target: el, props }) 20 | }, 21 | progress: { 22 | color: "#4B5563", 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/themes/svelte/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import leaf from "@leafphp/vite-plugin"; 3 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | leaf({ 8 | input: ['app/views/js/app.js', 'app/views/css/app.css'], 9 | refresh: true, 10 | }), 11 | svelte(), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /src/themes/tailwind/view/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @source "../"; 3 | -------------------------------------------------------------------------------- /src/themes/tailwind/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import leaf from "@leafphp/vite-plugin"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | tailwindcss(), 8 | leaf({ 9 | input: ["js/app.js", "css/app.css"], 10 | refresh: true, 11 | }), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /src/themes/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import leaf from '@leafphp/vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | leaf({ 7 | input: ['js/app.js', 'css/app.css'], 8 | refresh: true, 9 | }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /src/themes/vue/routes/_frontend.php: -------------------------------------------------------------------------------- 1 | get('/hello', function () { 4 | inertia('Hello'); 5 | }); 6 | -------------------------------------------------------------------------------- /src/themes/vue/views/_inertia.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | dispatch($page); 13 | } 14 | 15 | if ($__inertiaSsrResponse) { 16 | echo $__inertiaSsrResponse->head; 17 | } 18 | ?> 19 | 20 | 21 | 22 | dispatch($page); 26 | } 27 | 28 | if ($__inertiaSsrResponse) { 29 | echo $__inertiaSsrResponse->body; 30 | } else { 31 | echo '
'; 32 | } 33 | ?> 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/themes/vue/views/js/Pages/Hello.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/themes/vue/views/js/app.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from "vue"; 2 | import { createInertiaApp } from "@inertiajs/vue3"; 3 | import { resolvePageComponent } from "@leafphp/vite-plugin/inertia-helpers"; 4 | 5 | const appName = import.meta.env.VITE_APP_NAME || "Leaf PHP"; 6 | 7 | createInertiaApp({ 8 | title: (title) => `${title} - ${appName}`, 9 | resolve: (name) => 10 | resolvePageComponent( 11 | `./Pages/${name}.vue`, 12 | import.meta.glob("./Pages/**/*.vue") 13 | ), 14 | setup({ el, App, props, plugin }) { 15 | return createApp({ render: () => h(App, props) }) 16 | .use(plugin) 17 | .mount(el); 18 | }, 19 | progress: { 20 | color: "#4B5563", 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/themes/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import leaf from "@leafphp/vite-plugin"; 3 | import vue from "@vitejs/plugin-vue"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | leaf({ 8 | input: ["app/views/js/app.jsx"], 9 | refresh: true, 10 | }), 11 | vue({ 12 | template: { 13 | transformAssetUrls: { 14 | base: null, 15 | includeAbsolute: false, 16 | }, 17 | }, 18 | }), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /src/ui/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 4 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = false 7 | insert_final_newline = true 8 | quote_type = single 9 | -------------------------------------------------------------------------------- /src/ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | vendor 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/ui/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "symfony/process": "^6.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/ui/dist/assets/index-9259d9ea.css: -------------------------------------------------------------------------------- 1 | *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.right-10{right:2.5rem}.right-2{right:.5rem}.top-10{top:2.5rem}.top-2{top:.5rem}.m-0{margin:0}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-10{margin-top:2.5rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.flex{display:flex}.table{display:table}.grid{display:grid}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-5{height:1.25rem}.h-\[80vh\]{height:80vh}.h-full{height:100%}.h-screen{height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2\/3{width:66.666667%}.w-5{width:1.25rem}.w-full{width:100%}.w-screen{width:100vw}.max-w-\[200px\]{max-width:200px}.max-w-\[650px\]{max-width:650px}.max-w-none{max-width:none}.table-auto{table-layout:auto}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-10{gap:2.5rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.overflow-x-hidden{overflow-x:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-200\/10{border-color:#bfdbfe1a}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-gray-700\/25{border-color:#37415140}.border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity))}.border-green-900\/5{border-color:#14532d0d}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.bg-\[\#300f0f\]{--tw-bg-opacity: 1;background-color:rgb(48 15 15 / var(--tw-bg-opacity))}.bg-\[\#3eaf7c\]{--tw-bg-opacity: 1;background-color:rgb(62 175 124 / var(--tw-bg-opacity))}.bg-black\/25{background-color:#00000040}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-100\/20{background-color:#f3f4f633}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity: 1;background-color:rgb(22 101 52 / var(--tw-bg-opacity))}.bg-green-900\/5{background-color:#14532d0d}.bg-green-900\/50{background-color:#14532d80}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-yellow-700\/\[0\.5\]{background-color:#a1620780}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-10{padding-bottom:2.5rem}.pb-12{padding-bottom:3rem}.pl-5{padding-left:1.25rem}.pr-10{padding-right:2.5rem}.pt-2{padding-top:.5rem}.pt-20{padding-top:5rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.tracking-tight{letter-spacing:-.025em}.text-\[\#3eaf7c\]{--tw-text-opacity: 1;color:rgb(62 175 124 / var(--tw-text-opacity))}.text-\[\#aaa\]{--tw-text-opacity: 1;color:rgb(170 170 170 / var(--tw-text-opacity))}.text-\[\#f44336\]{--tw-text-opacity: 1;color:rgb(244 67 54 / var(--tw-text-opacity))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity))}.text-blue-50{--tw-text-opacity: 1;color:rgb(239 246 255 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.\[animation-delay\:75ms\]{animation-delay:75ms}.react-json-view{-ms-overflow-style:none;scrollbar-width:none}.react-json-view::-webkit-scrollbar{display:none}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}.hover\:border-gray-600\/25:hover{border-color:#4b556340}.hover\:border-green-600:hover{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity))}.hover\:bg-\[\#3eaf7c\]\/75:hover{background-color:#3eaf7cbf}.hover\:bg-black\/40:hover{background-color:#0006}.hover\:bg-green-900\/5:hover{background-color:#14532d0d}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.hover\:bg-slate-100\/5:hover{background-color:#f1f5f90d}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.focus\:bg-green-500:focus{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}@media (prefers-color-scheme: dark){.dark\:border-blue-200\/10{border-color:#bfdbfe1a}.dark\:border-blue-200\/20{border-color:#bfdbfe33}.dark\:border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity))}.dark\:border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.dark\:bg-gray-900\/25{background-color:#11182740}.dark\:bg-gray-900\/50{background-color:#11182780}.dark\:bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity))}.dark\:bg-transparent{background-color:transparent}.dark\:text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.dark\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\:text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:px-10{padding-left:2.5rem;padding-right:2.5rem}} 2 | -------------------------------------------------------------------------------- /src/ui/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Leaf CLI 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 19 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/ui/dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Leaf CLI 8 | 9 | 10 |
11 | 12 | 13 | 17 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtools-web", 3 | "private": true, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "glassx": "^1.0.25", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-fade-in": "^2.0.1", 16 | "react-feather": "^2.0.10", 17 | "react-json-view": "^1.21.3", 18 | "swr": "^2.1.3", 19 | "tailwind-merge": "^1.14.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@typescript-eslint/eslint-plugin": "^5.57.1", 25 | "@typescript-eslint/parser": "^5.57.1", 26 | "@vitejs/plugin-react-swc": "^3.0.0", 27 | "autoprefixer": "^10.4.14", 28 | "eslint": "^8.38.0", 29 | "eslint-plugin-react-hooks": "^4.6.0", 30 | "eslint-plugin-react-refresh": "^0.3.4", 31 | "postcss": "^8.4.23", 32 | "tailwindcss": "^3.3.2", 33 | "typescript": "^5.0.2", 34 | "vite": "^4.3.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import GlassX, { PersistedState, useStore } from 'glassx'; 2 | 3 | import Router from './utils/router'; 4 | 5 | // [TODO] Fix all `any` types later 6 | 7 | GlassX.store({ 8 | state: { 9 | screen: 'Home', 10 | }, 11 | plugins: [ 12 | new PersistedState({ 13 | key: 'leaf-devtools', 14 | exclude: ['screen'], 15 | }), 16 | ], 17 | }); 18 | 19 | function App() { 20 | const [screen] = useStore('screen'); 21 | 22 | return Router(screen); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/ui/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/src/components/@types/DirectoryInput.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react"; 2 | 3 | export interface DirectoryInputProps { 4 | dir: string; 5 | setDir: Dispatch>; 6 | configMutate: VoidFunction; 7 | loading: boolean; 8 | setLoading: Dispatch>; 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/src/components/@types/InlineForm.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react"; 2 | 3 | export interface InlineFormProps { 4 | placeholder?: string; 5 | value: string; 6 | setValue: Dispatch>; 7 | onSubmit: VoidFunction; 8 | } 9 | -------------------------------------------------------------------------------- /src/ui/src/components/@types/PageLayout.ts: -------------------------------------------------------------------------------- 1 | export interface PageLayoutProps { 2 | className?: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/ui/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from 'tailwind-merge'; 2 | 3 | const Card = ({ children, className, ...props }: any) => { 4 | return ( 5 |
12 | {children} 13 |
14 | ); 15 | }; 16 | 17 | export default Card; 18 | -------------------------------------------------------------------------------- /src/ui/src/components/ConsoleCard.tsx: -------------------------------------------------------------------------------- 1 | const ConsoleCard = ({ children, type }: any) => { 2 | return ( 3 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default ConsoleCard; 19 | -------------------------------------------------------------------------------- /src/ui/src/components/DirectoryInput.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useState } from 'react'; 2 | import { Check, X } from 'react-feather'; 3 | import { DirectoryInputProps } from './@types/DirectoryInput'; 4 | import { twMerge } from 'tailwind-merge'; 5 | 6 | const DirectoryInput: React.FC> = ({ 7 | dir, 8 | setDir, 9 | configMutate, 10 | loading, 11 | setLoading, 12 | }) => { 13 | const [isError, setIsError] = useState(false); 14 | 15 | return ( 16 |
17 | { 25 | const data = e.target.value; 26 | 27 | setDir(data); 28 | 29 | if ( 30 | !/^\/(?:[\w.-]+\/)*[\w.-]+$/.test(data) && 31 | data !== '' 32 | ) { 33 | setIsError(true); 34 | } else { 35 | setIsError(false); 36 | } 37 | }} 38 | /> 39 | 76 |
77 | ); 78 | }; 79 | 80 | export default DirectoryInput; 81 | -------------------------------------------------------------------------------- /src/ui/src/components/InlineForm.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from 'tailwind-merge'; 2 | import { Check, X } from 'react-feather'; 3 | import { PropsWithChildren, useState } from 'react'; 4 | 5 | import { InlineFormProps } from './@types/InlineForm'; 6 | 7 | const InlineForm: React.FC> = ({ 8 | value, 9 | setValue, 10 | onSubmit, 11 | placeholder = '', 12 | }) => { 13 | const [loading, setLoading] = useState(false); 14 | const [isError, setIsError] = useState(false); 15 | 16 | return ( 17 |
18 | { 27 | if (e.key === 'Enter') { 28 | onSubmit(); 29 | } 30 | }} 31 | onChange={(e) => { 32 | const data = e.target.value; 33 | 34 | setValue(data); 35 | 36 | if (data === '') { 37 | setIsError('Please enter a value'); 38 | } else { 39 | setIsError(false); 40 | } 41 | }} 42 | /> 43 | 68 |
69 | ); 70 | }; 71 | 72 | export default InlineForm; 73 | -------------------------------------------------------------------------------- /src/ui/src/components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'glassx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | const screens = [{ name: 'Home' }, { name: 'Create' }, { name: 'Apps' }]; 5 | 6 | const Nav = () => { 7 | const [screen, setScreen] = useStore('screen'); 8 | 9 | return ( 10 | 32 | ); 33 | }; 34 | 35 | export default Nav; 36 | -------------------------------------------------------------------------------- /src/ui/src/components/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'glassx'; 2 | import FadeIn from 'react-fade-in'; 3 | import { XCircle } from 'react-feather'; 4 | import { twMerge } from 'tailwind-merge'; 5 | 6 | import { PageLayoutProps } from './@types/PageLayout'; 7 | 8 | const PageLayout: React.FC> = ({ 9 | children, 10 | className, 11 | }) => { 12 | const [, setScreen] = useStore('screen'); 13 | 14 | return ( 15 |
21 | setScreen('home')} 25 | /> 26 | 27 | {children} 28 | 29 |
30 | ); 31 | }; 32 | 33 | export default PageLayout; 34 | -------------------------------------------------------------------------------- /src/ui/src/data/walkthrough.tsx: -------------------------------------------------------------------------------- 1 | import { FileMinus, Folder, FolderPlus } from 'react-feather'; 2 | 3 | export const themes = [ 4 | { 5 | key: 'leaf', 6 | icon: FileMinus, 7 | name: 'BASIC LEAF THEME', 8 | description: 9 | 'A basic Leaf app with a single index.php file, the simplest and fastest way to get started with Leaf. You can further customize this theme to add some extra features.', 10 | }, 11 | { 12 | key: 'mvc', 13 | icon: FolderPlus, 14 | name: 'LEAF MVC THEME', 15 | description: 16 | 'Leaf MVC is a simple MVC framework for Leaf. It provides a solid base for building complex web apps quickly. It is designed to be simple, lightweight and easy to learn.', 17 | }, 18 | { 19 | key: 'api', 20 | icon: Folder, 21 | name: 'LEAF API THEME', 22 | description: 23 | 'Leaf API is a simple MVC framework for Leaf specially crafted for building APIs. It provides a solid base for building complex APIs quickly.', 24 | }, 25 | ]; 26 | 27 | export const additionalFrontendOptions = [ 28 | { 29 | key: 'vite', 30 | icon: , 31 | name: 'Leaf + Vite', 32 | description: 'Bundle your app assets with Vite.', 33 | }, 34 | { 35 | key: 'tailwind', 36 | icon: ( 37 | 41 | ), 42 | name: 'Tailwind CSS', 43 | description: 'Set up Tailwind in your Leaf app.', 44 | }, 45 | ]; 46 | 47 | export const containers = [ 48 | { 49 | key: 'none', 50 | icon: ( 51 | 55 | ), 56 | name: 'No Container', 57 | description: 'Skip containerization. You can always add it later.', 58 | }, 59 | { 60 | key: 'docker', 61 | icon: ( 62 | 66 | ), 67 | name: 'Docker', 68 | description: 'Create a Docker container for your app.', 69 | }, 70 | ]; 71 | 72 | export const frontendFrameworks = [ 73 | { 74 | key: 'none', 75 | icon: ( 76 | 80 | ), 81 | name: 'Use template engine', 82 | description: 83 | 'Render UIs on the server using your selected templating engine', 84 | }, 85 | { 86 | key: 'react', 87 | icon: ( 88 | 92 | ), 93 | name: 'React JS', 94 | description: 'The library for web and native user interfaces', 95 | }, 96 | { 97 | key: 'vue', 98 | icon: , 99 | name: 'Vue JS', 100 | description: 'The Progressive JavaScript Framework', 101 | }, 102 | ]; 103 | 104 | export const modules = [ 105 | { 106 | key: 'none', 107 | name: 'None', 108 | description: 'Add no extra modules to your app.', 109 | }, 110 | { 111 | key: 'db', 112 | name: 'Database', 113 | description: 'Install Leaf DB in your app.', 114 | }, 115 | { 116 | key: 'auth', 117 | name: 'Authentication', 118 | description: 'Install Leaf Auth in your app.', 119 | }, 120 | { 121 | key: 'session', 122 | name: 'Session', 123 | description: 'Install Leaf Session in your app.', 124 | }, 125 | { 126 | key: 'cookie', 127 | name: 'Cookie', 128 | description: 'Install Leaf Cookie in your app.', 129 | }, 130 | { 131 | key: 'cors', 132 | name: 'Cors', 133 | description: 'Install Leaf Cors in your app.', 134 | }, 135 | { 136 | key: 'date', 137 | name: 'Date', 138 | description: 'Install Leaf Date in your app.', 139 | }, 140 | ]; 141 | 142 | export const templateEngines = [ 143 | { 144 | key: 'bare-ui', 145 | icon: ( 146 | 150 | ), 151 | name: 'Bare UI', 152 | description: 153 | 'Barebones templating engine built for speed and efficiency.', 154 | }, 155 | { 156 | key: 'blade', 157 | icon: ( 158 | 162 | ), 163 | name: 'Laravel Blade', 164 | description: "Laravel's powerful and flexible templating engine.", 165 | }, 166 | ]; 167 | 168 | export const testingFrameworks = [ 169 | { 170 | key: 'none', 171 | icon: ( 172 | 176 | ), 177 | name: 'No Tests', 178 | description: 179 | 'Exclude testing from your app. You can always add it later.', 180 | }, 181 | { 182 | key: 'pest', 183 | icon: ( 184 | 188 | ), 189 | name: 'Pest PHP', 190 | description: 'The elegant PHP testing framework.', 191 | }, 192 | { 193 | key: 'phpunit', 194 | icon: ( 195 | 196 | ), 197 | name: 'PHPUnit', 198 | description: 'The PHP Testing Framework.', 199 | }, 200 | ]; 201 | -------------------------------------------------------------------------------- /src/ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .react-json-view { 6 | -ms-overflow-style: none; 7 | scrollbar-width: none; 8 | } 9 | 10 | .react-json-view::-webkit-scrollbar { 11 | display: none; 12 | } 13 | 14 | :root { 15 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 16 | line-height: 1.5; 17 | font-weight: 400; 18 | 19 | color-scheme: light dark; 20 | color: rgba(255, 255, 255, 0.87); 21 | background-color: #242424; 22 | 23 | font-synthesis: none; 24 | text-rendering: optimizeLegibility; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | -webkit-text-size-adjust: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | import './index.css'; 5 | import { SWRConfig } from 'swr'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 13 | fetch(resource, init).then((res) => res.json()), 14 | }} 15 | > 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/ui/src/pages/@types/CreateScreen.ts: -------------------------------------------------------------------------------- 1 | export interface WalkthroughSelections { 2 | name?: string; 3 | type?: 'leaf' | 'mvc' | 'api'; 4 | templateEngine?: 'blade' | 'bare-ui'; 5 | frontendFramework?: 'react' | 'vue'; 6 | additionalFrontendOptions?: string[]; 7 | modules?: string[]; 8 | docker?: boolean; 9 | testing?: 'pest' | 'phpunit'; 10 | review?: boolean; 11 | directory?: string; 12 | } 13 | 14 | export type WalkthroughSteps = keyof WalkthroughSelections; 15 | export type ProjectType = WalkthroughSelections['type']; 16 | export type TemplateEngine = WalkthroughSelections['templateEngine']; 17 | export type FrontendFramework = WalkthroughSelections['frontendFramework']; 18 | export type AdditionalFrontendOptions = 19 | WalkthroughSelections['additionalFrontendOptions']; 20 | export type TestingFramework = WalkthroughSelections['testing']; 21 | 22 | export interface CreateSubScreenProps { 23 | values: WalkthroughSelections; 24 | navigate: (step: WalkthroughSteps) => void; 25 | setValues: (values: WalkthroughSelections) => void; 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/src/pages/AppsScreen.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import { useStore } from 'glassx'; 3 | import { Info, Terminal } from 'react-feather'; 4 | 5 | import ConsoleCard from '../components/ConsoleCard'; 6 | import PageLayout from '../components/PageLayout'; 7 | import { useEffect } from 'react'; 8 | 9 | const AppsScreen = () => { 10 | const [data, setData] = useStore('data'); 11 | const [url] = useStore('url'); 12 | 13 | const { data: appData, error } = useSWR(`${url}/leafDevToolsEventHook`); 14 | const consoleData = data?.console; 15 | 16 | useEffect(() => { 17 | if (appData) { 18 | setData(appData); 19 | } 20 | }, [appData, error]); 21 | 22 | const clearConsole = () => { 23 | fetch(`${url}/leafDevToolsEventHook?action=clearLogs`).then((res) => { 24 | if (res.ok) { 25 | setData(res.json()); 26 | } else { 27 | console.error('Could not clear console logs.'); 28 | } 29 | }); 30 | }; 31 | 32 | return ( 33 | 34 |
35 |
36 | 37 |
38 |

DevTools Console

39 |
40 | {consoleData?.length} Server Log 41 | {consoleData?.length !== 1 && 's'} 42 |
43 |
44 |
45 |

46 | The devtools console 47 | provides a simple way to log out data for quick and easy 48 | debugging, just as you would do with console.log in 49 | JavaScript. 50 |

51 |
52 | 53 |
54 | 55 | {consoleData?.length > 0 ? ( 56 | 62 | ) : ( 63 |
64 |
There's no console data to show.
65 |
66 | You can log items out using the 67 | Leaf\DevTools::console() method. 68 |
69 | 70 |
71 |                                 
72 |                                     Leaf\DevTools::console('console.log this
73 |                                     data');
74 |                                 
75 |                             
76 |
77 | )} 78 |
79 | {consoleData?.map((item: any, index: number) => ( 80 | 81 | {typeof item[1] === 'string' 82 | ? item[1] 83 | : JSON.stringify(item[1])} 84 | 85 | ))} 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default AppsScreen; 92 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/AdditionalFrontendOptionsScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { additionalFrontendOptions } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps } from '../@types/CreateScreen'; 4 | 5 | const AdditionalFrontendOptionsScreen: React.FC< 6 | React.PropsWithChildren 7 | > = ({ values, navigate, setValues }) => { 8 | return ( 9 | <> 10 |
11 |
12 |

13 | Choose Frontend Add-ons 14 |

15 |
16 | Leaf will automatically install and configure selected 17 | packages 18 |
19 |
20 |
21 | 22 |
23 | {additionalFrontendOptions 24 | .filter(({ key }) => { 25 | if (values.type === 'mvc') { 26 | return key !== 'vite'; 27 | } 28 | 29 | return true; 30 | }) 31 | .map(({ icon, key, name, description }) => ( 32 | { 40 | if ( 41 | values.additionalFrontendOptions?.includes( 42 | key 43 | ) 44 | ) { 45 | setValues({ 46 | ...values, 47 | additionalFrontendOptions: 48 | values.additionalFrontendOptions?.filter( 49 | (option) => option !== key 50 | ), 51 | }); 52 | } else { 53 | setValues({ 54 | ...values, 55 | additionalFrontendOptions: [ 56 | ...(values.additionalFrontendOptions ?? 57 | []), 58 | key, 59 | ], 60 | }); 61 | } 62 | }} 63 | > 64 |

65 | {icon} {name} 66 |

67 |

68 | {description} 69 |

70 |
71 | ))} 72 | 80 |
81 | 82 | ); 83 | }; 84 | 85 | export default AdditionalFrontendOptionsScreen; 86 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/AppTypeScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { themes } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps, ProjectType } from '../@types/CreateScreen'; 4 | 5 | const AppTypeScreen: React.FC< 6 | React.PropsWithChildren 7 | > = ({ values, navigate, setValues }) => { 8 | return ( 9 | <> 10 |
11 |
12 |

Choose a starter kit

13 |
14 | What kind of Leaf app do you want to create? 15 |
16 |
17 |
18 | 19 |
20 | {themes.map(({ icon: Icon, key, name, description }) => ( 21 | { 29 | setValues({ ...values, type: key as ProjectType }); 30 | navigate( 31 | key === 'api' ? 'testing' : 'templateEngine' 32 | ); 33 | }} 34 | > 35 |

36 | {name} 37 |

38 |

39 | {description} 40 |

41 |
42 | ))} 43 |
44 | 45 | ); 46 | }; 47 | 48 | export default AppTypeScreen; 49 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/DockerScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { containers } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps } from '../@types/CreateScreen'; 4 | 5 | const DockerScreen: React.FC> = ({ 6 | values, 7 | navigate, 8 | setValues, 9 | }) => { 10 | return ( 11 | <> 12 |
13 |
14 |

15 | Choose a Container Solution 16 |

17 |
18 | This option allows you to containerize your app with 19 |
20 |
21 |
22 | 23 |
24 | {containers.map(({ icon, key, name, description }) => ( 25 | { 33 | if (key === 'docker') { 34 | setValues({ 35 | ...values, 36 | docker: true, 37 | }); 38 | } 39 | 40 | navigate('review'); 41 | }} 42 | > 43 |

44 | {icon} {name} 45 |

46 |

47 | {description} 48 |

49 |
50 | ))} 51 |
52 | 53 | ); 54 | }; 55 | 56 | export default DockerScreen; 57 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/FrontendFrameworkScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { frontendFrameworks } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps, FrontendFramework } from '../@types/CreateScreen'; 4 | 5 | const FrontendFrameworkScreen: React.FC> = ({ 6 | values, 7 | navigate, 8 | setValues, 9 | }) => { 10 | return ( 11 | <> 12 |
13 |
14 |

15 | Choose a Frontend Framework 16 |

17 |
18 | Leaf will use this framework to render your UI. 19 |
20 |
21 |
22 | 23 |
24 | {frontendFrameworks.map(({ icon, key, name, description }) => ( 25 | { 33 | if (key !== 'none') { 34 | setValues({ 35 | ...values, 36 | frontendFramework: key as FrontendFramework, 37 | }); 38 | } 39 | 40 | navigate('additionalFrontendOptions'); 41 | }} 42 | > 43 |

44 | {icon} {name} 45 |

46 |

47 | {description} 48 |

49 |
50 | ))} 51 |
52 | 53 | ); 54 | }; 55 | 56 | export default FrontendFrameworkScreen; 57 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/ModulesScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { modules } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps } from '../@types/CreateScreen'; 4 | 5 | const ModulesScreen: React.FC< 6 | React.PropsWithChildren 7 | > = ({ values, navigate, setValues }) => { 8 | return ( 9 | <> 10 |
11 |
12 |

Add leaf modules

13 |
14 | Select modules to add to your leaf app 15 |
16 |
17 |
18 | 19 |
20 | {modules.map(({ key, name, description }) => ( 21 | { 29 | if (key === 'none') { 30 | setValues({ 31 | ...values, 32 | modules: ['none'], 33 | }); 34 | } else { 35 | if (values.modules?.includes(key)) { 36 | setValues({ 37 | ...values, 38 | modules: values.modules?.filter( 39 | (option) => 40 | option !== key && 41 | option !== 'none' 42 | ), 43 | }); 44 | } else { 45 | setValues({ 46 | ...values, 47 | modules: [ 48 | ...(values.modules?.filter( 49 | (option) => option !== 'none' 50 | ) ?? []), 51 | key, 52 | ], 53 | }); 54 | } 55 | } 56 | }} 57 | > 58 |

59 | {name} 60 |

61 |

62 | {description} 63 |

64 |
65 | ))} 66 |
67 | 68 |
69 | 75 |
76 | 77 | ); 78 | }; 79 | 80 | export default ModulesScreen; 81 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/NameScreen.tsx: -------------------------------------------------------------------------------- 1 | import InlineForm from '../../components/InlineForm'; 2 | import { CreateSubScreenProps } from '../@types/CreateScreen'; 3 | 4 | const NameScreen: React.FC> = ({ 5 | values, 6 | navigate, 7 | setValues, 8 | }) => { 9 | return ( 10 | <> 11 |
12 |
13 |

Create Application

14 |
15 | What would you like to name your application? 16 |
17 |
18 |
19 | 20 |
21 | 24 | setValues({ ...values, name: value as string }) 25 | } 26 | placeholder="Application Name" 27 | onSubmit={() => { 28 | navigate('type'); 29 | }} 30 | /> 31 |
32 | 33 | ); 34 | }; 35 | 36 | export default NameScreen; 37 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/ReviewScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useStore } from 'glassx'; 3 | 4 | import Card from '../../components/Card'; 5 | import { CreateSubScreenProps } from '../@types/CreateScreen'; 6 | import { 7 | additionalFrontendOptions, 8 | containers, 9 | frontendFrameworks, 10 | modules, 11 | templateEngines, 12 | testingFrameworks, 13 | themes, 14 | } from '../../data/walkthrough'; 15 | 16 | const ReviewScreen: React.FC> = ({ 17 | values, 18 | navigate, 19 | setValues, 20 | }) => { 21 | const [error, setError] = useState(false); 22 | const [loading, setLoading] = useState(false); 23 | const [success, setSuccess] = useState(false); 24 | 25 | const appType = themes.find((theme) => theme.key === values.type); 26 | const templateEngine = templateEngines.find( 27 | (engine) => engine.key === values.templateEngine 28 | ); 29 | const frontendFramework = frontendFrameworks.find( 30 | (framework) => framework.key === values.frontendFramework 31 | ); 32 | const testingFramework = testingFrameworks.find( 33 | (framework) => framework.key === values.testing 34 | ); 35 | 36 | const createApp = () => { 37 | setLoading(true); 38 | 39 | const formData = { 40 | ...values, 41 | name: values?.name?.trim().replace(/\s+/g, '-').toLowerCase(), 42 | }; 43 | 44 | if ( 45 | formData.frontendFramework || 46 | formData.additionalFrontendOptions?.includes('tailwind') 47 | ) { 48 | formData.additionalFrontendOptions = 49 | formData.additionalFrontendOptions?.filter( 50 | (option) => option !== 'vite' 51 | ); 52 | } 53 | 54 | fetch(`${window.location.origin}/server.php?action=createApp`, { 55 | method: 'POST', 56 | body: JSON.stringify({ 57 | data: JSON.stringify(formData), 58 | }), 59 | }) 60 | .then((res) => { 61 | if (res.ok) { 62 | return res.json(); 63 | } 64 | }) 65 | .then((response) => { 66 | if (response?.status === 'success') { 67 | setValues({ 68 | ...values, 69 | ...response?.data, 70 | }); 71 | 72 | setSuccess(true); 73 | } else { 74 | setError(response?.message); 75 | } 76 | }) 77 | .catch((err) => { 78 | console.log('An error occurred', err); 79 | }) 80 | .finally(() => { 81 | setLoading(false); 82 | }); 83 | }; 84 | 85 | return loading ? ( 86 | 87 | ) : success ? ( 88 | 93 | ) : error ? ( 94 | 100 | ) : ( 101 | <> 102 |
103 |
104 |

105 | Review your app config 106 |

107 |
108 | Make sure everything looks good before creating your 109 | app. 110 |
111 |
112 |
113 | 114 |
115 |

Your application type

116 | {appType && ( 117 | { 121 | navigate('type'); 122 | }} 123 | > 124 |

125 | {appType.name} 126 |

127 |

128 | {appType.description} 129 |

130 |
131 | )} 132 |
133 | 134 | {values.type !== 'api' && ( 135 | <> 136 |
137 |

Your template engine

138 | {templateEngine && ( 139 | { 143 | navigate('templateEngine'); 144 | }} 145 | > 146 |

147 | {templateEngine.icon} {templateEngine.name} 148 |

149 |

150 | {templateEngine.description} 151 |

152 |
153 | )} 154 |
155 | 156 | {values.frontendFramework && ( 157 |
158 |

159 | Your frontend framework 160 |

161 | {frontendFramework && ( 162 | { 166 | navigate('frontendFramework'); 167 | }} 168 | > 169 |

170 | {frontendFramework.icon}{' '} 171 | {frontendFramework.name} 172 |

173 |

174 | {frontendFramework.description} 175 |

176 |
177 | )} 178 |
179 | )} 180 | 181 | {(values.additionalFrontendOptions?.length ?? 0) > 0 && ( 182 |
183 |

184 | Additional Frontend Options 185 |

186 | {values.additionalFrontendOptions?.map((option) => { 187 | const item = additionalFrontendOptions.find( 188 | (o) => o.key === option 189 | ); 190 | 191 | return item ? ( 192 | { 196 | navigate( 197 | 'additionalFrontendOptions' 198 | ); 199 | }} 200 | > 201 |

202 | {item.icon} {item.name} 203 |

204 |

205 | {item.description} 206 |

207 |
208 | ) : ( 209 | <> 210 | ); 211 | })} 212 |
213 | )} 214 | 215 | {values.type === 'leaf' && values.modules && ( 216 |
217 |

Selected Modules

218 | {values.modules?.map((option) => { 219 | const item = modules.find( 220 | (o) => o.key === option 221 | ); 222 | 223 | return item ? ( 224 | { 228 | navigate('modules'); 229 | }} 230 | > 231 |

232 | {item.name} 233 |

234 |

235 | {item.description} 236 |

237 |
238 | ) : ( 239 | <> 240 | ); 241 | })} 242 |
243 | )} 244 | 245 | )} 246 | 247 | {values.testing && ( 248 |
249 |

Your testing framework

250 | {testingFramework ? ( 251 | { 255 | navigate('testing'); 256 | }} 257 | > 258 |

259 | {testingFramework.icon} {testingFramework.name} 260 |

261 |

262 | {testingFramework.description} 263 |

264 |
265 | ) : ( 266 | { 270 | navigate('testing'); 271 | }} 272 | > 273 |

274 | None 275 |

276 |

277 | You can always add a testing framework later. 278 |

279 |
280 | )} 281 |
282 | )} 283 | 284 |
285 |

Selected container solution

286 | {!values.docker && ( 287 | { 291 | navigate('docker'); 292 | }} 293 | > 294 |

295 | None 296 |

297 |

298 | You can always add a container solution later. 299 |

300 |
301 | )} 302 | {containers 303 | ?.filter((theme) => theme.key === 'docker' && values.docker) 304 | .map(({ icon, key, name, description }) => ( 305 | { 309 | navigate('docker'); 310 | }} 311 | > 312 |

313 | {icon} {name} 314 |

315 |

316 | {description} 317 |

318 |
319 | ))} 320 |
321 | 322 |
323 | 329 |
330 | 331 | ); 332 | }; 333 | 334 | const LoadingSection = () => { 335 | return ( 336 |
337 |

338 | Creating your Leaf app 339 |

340 |
341 |
.
342 |
.
343 |
.
344 |
345 |
346 | ); 347 | }; 348 | 349 | const SuccessSection: React.FC = ({ values }) => { 350 | const [, setScreen] = useStore('screen'); 351 | 352 | return ( 353 |
354 |
355 |

356 | Your {values.type} app has been created! 357 |

358 |

359 | To get started, you can follow these steps: 360 |

361 |
362 | 363 |
364 |                 
365 |
$
366 |
367 | cd{' '} 368 | {values?.directory 369 | ? `${values?.directory}/${values?.name}` 370 | : values.name} 371 |
372 |
373 | {!!( 374 | values?.additionalFrontendOptions || 375 | values?.frontendFramework 376 | ) && ( 377 |
378 |
$
379 |
leaf view:dev
380 |
381 | )} 382 |
383 |
$
384 |
leaf serve
385 |
386 |
387 | 388 |
389 | 397 |
398 |
399 | ); 400 | }; 401 | 402 | const ErrorSection: React.FC< 403 | CreateSubScreenProps & { error: boolean | string } 404 | > = ({ error, navigate }) => { 405 | return ( 406 |
407 |
408 |

Your app could not be created!

409 |

{error}

410 |
411 | 412 |
413 | 421 |
422 |
423 | ); 424 | }; 425 | 426 | export default ReviewScreen; 427 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/TemplateEngineScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { templateEngines } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps, TemplateEngine } from '../@types/CreateScreen'; 4 | 5 | const TemplateEngineScreen: React.FC< 6 | React.PropsWithChildren 7 | > = ({ values, navigate, setValues }) => { 8 | return ( 9 | <> 10 |
11 |
12 |

Choose a UI Engine

13 |
14 | This is the engine that will be used to render your UI. 15 |
16 |
17 |
18 | 19 |
20 | {templateEngines.map(({ icon, key, name, description }) => ( 21 | { 29 | setValues({ 30 | ...values, 31 | templateEngine: key as TemplateEngine, 32 | }); 33 | navigate('frontendFramework'); 34 | }} 35 | > 36 |

37 | {icon} {name} 38 |

39 |

40 | {description} 41 |

42 |
43 | ))} 44 |
45 | 46 | ); 47 | }; 48 | 49 | export default TemplateEngineScreen; 50 | -------------------------------------------------------------------------------- /src/ui/src/pages/Create/TestingScreen.tsx: -------------------------------------------------------------------------------- 1 | import Card from '../../components/Card'; 2 | import { testingFrameworks } from '../../data/walkthrough'; 3 | import { CreateSubScreenProps, TestingFramework } from '../@types/CreateScreen'; // prettier-ignore 4 | 5 | const TestingScreen: React.FC< 6 | React.PropsWithChildren 7 | > = ({ values, navigate, setValues }) => { 8 | return ( 9 | <> 10 |
11 |
12 |

13 | Choose a Testing Framework 14 |

15 |
16 | Leaf will use this framework to create and run tests 17 |
18 |
19 |
20 | 21 |
22 | {testingFrameworks.map(({ icon, key, name, description }) => ( 23 | { 31 | if (key !== 'none') { 32 | setValues({ 33 | ...values, 34 | testing: key as TestingFramework, 35 | }); 36 | } 37 | 38 | navigate('docker'); 39 | }} 40 | > 41 |

42 | {icon} {name} 43 |

44 |

45 | {description} 46 |

47 |
48 | ))} 49 |
50 | 51 | ); 52 | }; 53 | 54 | export default TestingScreen; 55 | -------------------------------------------------------------------------------- /src/ui/src/pages/CreateScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import NameScreen from './Create/NameScreen'; 4 | import PageLayout from '../components/PageLayout'; 5 | import AppTypeScreen from './Create/AppTypeScreen'; 6 | import TemplateEngineScreen from './Create/TemplateEngineScreen'; 7 | import FrontendFrameworkScreen from './Create/FrontendFrameworkScreen'; 8 | import { WalkthroughSelections, WalkthroughSteps } from './@types/CreateScreen'; 9 | import AdditionalFrontendOptionsScreen from './Create/AdditionalFrontendOptionsScreen'; 10 | import ModulesScreen from './Create/ModulesScreen'; 11 | import TestingScreen from './Create/TestingScreen'; 12 | import DockerScreen from './Create/DockerScreen'; 13 | import ReviewScreen from './Create/ReviewScreen'; 14 | 15 | const CreateScreen = () => { 16 | const [walkthrough, setWalkthrough] = useState('name'); 17 | const [selected, setSelected] = useState({ 18 | name: '', 19 | }); 20 | 21 | // [Todo] Refactor this later 22 | 23 | return ( 24 | 25 | {walkthrough === 'name' && ( 26 | 31 | )} 32 | 33 | {walkthrough === 'type' && ( 34 | 39 | )} 40 | 41 | {walkthrough === 'templateEngine' && ( 42 | 47 | )} 48 | 49 | {walkthrough === 'frontendFramework' && ( 50 | 55 | )} 56 | 57 | {walkthrough === 'additionalFrontendOptions' && ( 58 | 63 | )} 64 | 65 | {walkthrough === 'modules' && ( 66 | 71 | )} 72 | 73 | {walkthrough === 'testing' && ( 74 | 79 | )} 80 | 81 | {walkthrough === 'docker' && ( 82 | 87 | )} 88 | 89 | {walkthrough === 'review' && ( 90 | 95 | )} 96 | 97 | ); 98 | }; 99 | 100 | export default CreateScreen; 101 | -------------------------------------------------------------------------------- /src/ui/src/pages/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import { useState } from 'react'; 3 | import { useStore } from 'glassx'; 4 | import FadeIn from 'react-fade-in'; 5 | import { DownloadCloud, Terminal, Layers, Star, BookOpen, GitHub } from 'react-feather'; // prettier-ignore 6 | 7 | import Card from '../components/Card'; 8 | import DirectoryInput from '../components/DirectoryInput'; 9 | 10 | const HomeScreen = () => { 11 | const [, setScreen] = useStore('screen'); 12 | const [dir, setDir] = useState(''); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const { data: versionData } = useSWR( 16 | 'https://repo.packagist.org/p2/leafs/leaf.json' 17 | ); 18 | const { data: config, mutate: configMutate } = useSWR( 19 | `${window.location.origin}/server.php?action=getConfig` 20 | ); 21 | 22 | const leafInfo = versionData?.packages['leafs/leaf']; 23 | 24 | return ( 25 | 26 |
27 | logo 32 |
33 |
34 |

35 | Leaf CLI {ui} 36 |

37 | 38 | Beta 39 | 40 |
41 |

42 | v0.0.5 - 7 Oct, 2023 43 |

44 |
45 |
46 | 47 | {config?.data?.dir ? ( 48 |
49 | 50 | 51 |
52 | {leafInfo?.[0]?.version} 53 |
54 |

55 | Latest Leaf Version 56 |

57 |
58 | setScreen('Create')} 61 | > 62 | 63 |
64 | Create 65 |
66 |

67 | Setup a new Leaf app 68 |

69 |
70 | 71 | 72 |
Apps
73 |

74 | Coming Soon 75 |

76 |
77 |
78 | ) : ( 79 |
80 |
81 |

82 | We noticed this is your first time using the UI. To 83 | get started, you need to configure a directory where 84 | Leaf will save all of the projects you create using 85 | the UI. You can always update the folder you select. 86 |

87 | 88 | 95 |
96 |
97 | )} 98 | 99 | {config?.data?.dir && ( 100 | 126 | )} 127 |
128 | ); 129 | }; 130 | 131 | export default HomeScreen; 132 | -------------------------------------------------------------------------------- /src/ui/src/pages/InsightsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'glassx'; 2 | import { Info, Package } from 'react-feather'; 3 | import ReactJson from 'react-json-view'; 4 | 5 | import PageLayout from '../components/PageLayout'; 6 | 7 | const InsightsScreen = () => { 8 | const [data] = useStore('data'); 9 | 10 | return ( 11 | 12 |
13 |
14 |
15 | 16 |
17 |

18 | Application Overview 19 |

20 |
21 | Config, session, env, etc. 22 |
23 |
24 |
25 |

26 | This screen provides 27 | insights into your application config, env, session, 28 | cookies, etc. 29 |

30 |
31 | 32 |
33 |

34 | App Config 35 |

36 | 46 |
47 | 48 |
49 |

50 | App Request 51 |

52 | 62 |
63 | 64 |
65 |

66 | $_SERVER 67 |

68 | 78 |
79 | 80 |
81 |

82 | Cookies 83 |

84 | 94 |
95 | 96 |
97 |

98 | Session 99 |

100 | 110 |
111 | 112 |
113 |

114 | Headers 115 |

116 | 126 |
127 | 128 |
129 |

130 | Env 131 |

132 | 142 |
143 |
144 |
145 | ); 146 | }; 147 | 148 | export default InsightsScreen; 149 | -------------------------------------------------------------------------------- /src/ui/src/pages/LeafNotFound.tsx: -------------------------------------------------------------------------------- 1 | const LeafNotFound = () => { 2 | return ( 3 |
4 |
5 | logo 10 |

11 | This application does not appear to be using Leaf 12 |

13 |
14 |
15 | ); 16 | }; 17 | 18 | export default LeafNotFound; 19 | -------------------------------------------------------------------------------- /src/ui/src/pages/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import { useStore } from 'glassx'; 3 | import { Loader } from 'react-feather'; 4 | 5 | const LoadingScreen = () => { 6 | // const [appUrl, setUrl] = useStore('url'); 7 | const [, setData] = useStore('data'); 8 | const [, setScreen] = useStore('screen'); 9 | const [, setAppUsesLeaf] = useStore('appUsesLeaf'); 10 | 11 | // if (!appUrl) { 12 | // chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { 13 | // let appUrl = tabs?.[0]?.url; 14 | 15 | // if (typeof appUrl === 'string') { 16 | // setUrl(new URL(appUrl).origin); 17 | // } 18 | // }); 19 | // } 20 | 21 | const { data: appData } = useSWR(`${/*appUrl*/ ''}/leafDevToolsEventHook`); 22 | 23 | if (appData) { 24 | setData(appData); 25 | setAppUsesLeaf(true); 26 | setScreen('HomeScreen'); 27 | } 28 | 29 | return ( 30 |
31 | 32 |
33 | ); 34 | }; 35 | 36 | export default LoadingScreen; 37 | -------------------------------------------------------------------------------- /src/ui/src/pages/RoutesScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'glassx'; 2 | import { Info, Layers } from 'react-feather'; 3 | 4 | import PageLayout from '../components/PageLayout'; 5 | 6 | const RoutesScreen = () => { 7 | const [data] = useStore('data'); 8 | const routes = data?.app?.routes; 9 | 10 | return ( 11 | 12 |
13 |
14 |
15 | 16 |
17 |

18 | Application Routes 19 |

20 |
21 | {routes?.length - 1} Route 22 | {routes?.length - 1 !== 1 && 's'} defined 23 |
24 |
25 |
26 |

27 | This screen provides 28 | insights into your application routes. 29 |

30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {routes 44 | ?.filter((i: any) => 45 | !i?.pattern?.includes( 46 | '/leafDevToolsEventHook' 47 | ) 48 | ) 49 | ?.map((route: any) => ( 50 | 51 | 55 | 58 | 61 | 66 | 67 | ))} 68 | 69 |
MethodsPatternNameHandler
52 | {route?.methods?.join?.(' | ') || 53 | '-'} 54 | 56 | {route?.pattern} 57 | 59 | {route?.name || 'N/A'} 60 | 62 | {typeof route?.handler === 'object' 63 | ? 'User Function' 64 | : route?.handler} 65 |
70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default RoutesScreen; 77 | -------------------------------------------------------------------------------- /src/ui/src/utils/fs.tsx: -------------------------------------------------------------------------------- 1 | export const openDirectoryPicker = async (mode = 'read') => { 2 | // Feature detection. The API needs to be supported 3 | // and the app not run in an iframe. 4 | const supportsFileSystemAccess = 5 | 'showDirectoryPicker' in window && 6 | (() => { 7 | try { 8 | return window.self === window.top; 9 | } catch { 10 | return false; 11 | } 12 | })(); 13 | 14 | // If the File System Access API is supported… 15 | if (supportsFileSystemAccess) { 16 | let directoryStructure = undefined; 17 | 18 | // Recursive function that walks the directory structure. 19 | // @ts-expect-error testing 20 | const getFiles = async (dirHandle, path = dirHandle.name) => { 21 | const dirs = []; 22 | const files = []; 23 | for await (const entry of dirHandle.values()) { 24 | const nestedPath = `${path}/${entry.name}`; 25 | 26 | if (entry.kind === 'file') { 27 | files.push( 28 | // @ts-expect-error Just testing 29 | entry.getFile().then((file) => { 30 | file.directoryHandle = dirHandle; 31 | file.handle = entry; 32 | return Object.defineProperty( 33 | file, 34 | 'webkitRelativePath', 35 | { 36 | configurable: true, 37 | enumerable: true, 38 | get: () => nestedPath, 39 | } 40 | ); 41 | }) 42 | ); 43 | } else if (entry.kind === 'directory') { 44 | dirs.push(getFiles(entry, nestedPath)); 45 | } 46 | } 47 | return [ 48 | ...(await Promise.all(dirs)).flat(), 49 | ...(await Promise.all(files)), 50 | ]; 51 | }; 52 | 53 | try { 54 | // Open the directory. 55 | // @ts-expect-error method might not be available in every browser 56 | const handle = await showDirectoryPicker({ 57 | mode, 58 | }); 59 | 60 | directoryStructure = getFiles(handle, undefined); 61 | } catch (err: any) { 62 | if (err.name !== 'AbortError') { 63 | console.error(err.name, err.message); 64 | } 65 | } 66 | return directoryStructure; 67 | } 68 | 69 | // Fallback if the File System Access API is not supported. 70 | return new Promise((resolve) => { 71 | const input = document.createElement('input'); 72 | input.type = 'file'; 73 | input.webkitdirectory = true; 74 | 75 | input.addEventListener('change', () => { 76 | // @ts-expect-error input.files 77 | const files = Array.from(input.files); 78 | resolve(files); 79 | }); 80 | 81 | if ('showPicker' in HTMLInputElement.prototype) { 82 | input.showPicker(); 83 | } else { 84 | input.click(); 85 | } 86 | }); 87 | }; 88 | 89 | export const FolderPickerButton = () => { 90 | return ( 91 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /src/ui/src/utils/router.tsx: -------------------------------------------------------------------------------- 1 | import HomeScreen from '../pages/HomeScreen'; 2 | import AppsScreen from '../pages/AppsScreen'; 3 | import CreateScreen from '../pages/CreateScreen'; 4 | import InsightsScreen from '../pages/InsightsScreen'; 5 | import LeafNotFound from '../pages/LeafNotFound'; 6 | import LoadingScreen from '../pages/LoadingScreen'; 7 | import RoutesScreen from '../pages/RoutesScreen'; 8 | 9 | const Router = (screen: string) => { 10 | switch (screen) { 11 | case 'Home': 12 | return ; 13 | case 'Apps': 14 | return ; 15 | case 'Create': 16 | return ; 17 | case 'Loading': 18 | return ; 19 | case 'Routes': 20 | return ; 21 | case 'Insights': 22 | return ; 23 | case 'LeafNotFound': 24 | return ; 25 | default: 26 | return ; 27 | } 28 | }; 29 | 30 | export default Router; 31 | -------------------------------------------------------------------------------- /src/ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /src/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /src/ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/ui/ui.config.json: -------------------------------------------------------------------------------- 1 | {"dir":"\/Users\/mychidarko\/Projects\/leaf-apps","phpDir":null} 2 | -------------------------------------------------------------------------------- /src/ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------