diff --git a/.github/workflows/matrix-tests.yml b/.github/workflows/matrix-tests.yml index 9fa499e0e5f..e08b02b921e 100644 --- a/.github/workflows/matrix-tests.yml +++ b/.github/workflows/matrix-tests.yml @@ -2,7 +2,7 @@ name: Matrix Tests on: pull_request: - branches: [ "master", "2.x-dev" ] + branches: [ "master", "3.x-dev" ] paths: - 'app/**' - 'packages/**' diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index a9ac5315d2f..764690037e1 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -2,7 +2,7 @@ name: ๐Ÿ”Ž Static Analysis on: pull_request: - branches: [ "master", "2.x-dev" ] + branches: [ "master", "3.x-dev" ] jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a6bd714a3..4b3f13e4f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,10 +23,10 @@ This serves two purposes: - for changes in existing functionality. ### Deprecated -- Deprecated the `rebuild` command. It has no remaining internal consumers now that the realtime compiler renders pages in-memory, and single-page builds can silently leave aggregate outputs (sitemap, RSS, search index, navigation) stale. It will be removed in v3.0; use `Hyde\Framework\Actions\StaticPageBuilder::handle()` instead if you need to build a single page programmatically. +- for changes that will be removed in upcoming releases. ### Removed -- for now removed features. +- Removed the `rebuild` command. It had no remaining internal consumers now that the realtime compiler renders pages in-memory, and single-page builds can silently leave aggregate outputs (sitemap, RSS, search index, navigation) stale. Use `Hyde\Framework\Actions\StaticPageBuilder::handle()` instead if you need to build a single page programmatically. ### Fixed - Improved documentation page detection in MarkdownService so it works for child classes in https://github.com/hydephp/develop/pull/2332 diff --git a/HYDEPHP_V3_PLANNING.md b/HYDEPHP_V3_PLANNING.md new file mode 100644 index 00000000000..c2923c23465 --- /dev/null +++ b/HYDEPHP_V3_PLANNING.md @@ -0,0 +1,33 @@ +# Welcome to the HydePHP v3 planning document! + +Having this document in code lets us know the devlopment state at any given point in the development tree. + +## Planned features + +## Changes requires to this branch + +## Changes required to the v2 branch + +--- + +## Release Notes + +### New Features + +### Feature Changes + +### Minor Changes and Cleanup + +- Removed the legacy `checkForDeprecatedRunMixCommandUsage` check and the placeholder `--run-dev`/`--run-prod` options from the `build` command, which were kept in v2 only to surface a helpful error message. ([#2461](https://github.com/hydephp/develop/pull/2461)) +- Removed the deprecated `hyde.server.dashboard` boolean config check from `DashboardController::enabled()`, which was kept in v2 for backwards compatibility but had since then been replaced with `hyde.server.dashboard.enabled`. ([#2461](https://github.com/hydephp/develop/pull/2462)) +- Upgraded the bundled `vite` dependency from v7 to v8, and widened the `hyde-vite-plugin`'s `vite` peer dependency range from `>=6.3.5 <8.0.0` to `>=6.3.5 <9.0.0` so downstream projects can adopt Vite 8 without waiting for a new plugin major. The plugin's build config now targets Vite 8's Rolldown-based bundler (`rolldownOptions` instead of `rollupOptions`). ([#2414](https://github.com/hydephp/develop/pull/2414)) + +### Breaking Changes + +- Removed the `rebuild` command (`RebuildPageCommand`). It was originally added to build a single file to disk before the realtime compiler existed, and later used internally by the RC to build-and-serve a path, but the RC now renders everything in-memory, leaving `rebuild` with no remaining consumer. It also had no safe user-facing use case: a single-page build only produces a correct `_site` when the page is self-contained, while a page change routinely invalidates aggregate outputs (sitemap, RSS, search index, post listings, navigation), so single-path building could silently leave a stale output directory that looked complete. The underlying single-page build capability remains available internally via the `StaticPageBuilder` action. ([#2490](https://github.com/hydephp/develop/pull/2490)) + +### Upgrade guide + +Please fill in UPGRADE.md as you make changes. + +- The `rebuild` command has been removed. If you need to build a single page programmatically, use `Hyde\Framework\Actions\StaticPageBuilder::handle()` instead. diff --git a/UPGRADE.md b/UPGRADE.md index 05d6332ea52..a79ee9e5e10 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,16 +1,14 @@ -# HydePHP v2.0 Upgrade Guide +# HydePHP v3.0 Upgrade Guide ## Overview -HydePHP v2.0 represents a major evolution of the framework, introducing significant improvements to the asset system, navigation API, and overall developer experience. This release modernizes the frontend tooling by replacing Laravel Mix with Vite, completely rewrites the navigation system for better flexibility, and introduces numerous performance optimizations throughout the framework. - -This document will guide you through the upgrade process. If you want to learn more about the new features, take a look at the [Release Notes](https://hydephp.com/docs/2.x/release-notes). +// ## Before You Begin ### Prerequisites -Before upgrading to HydePHP v2.0, ensure your application is running **HydePHP v1.6 or later** (ideally v1.8). These versions include transition helpers that will make the migration process smoother. +// ### Backup Your Project @@ -25,514 +23,92 @@ If you're not already using Git for version control, now is an excellent time to ```bash git init git add . -git commit -m "Pre-upgrade backup before HydePHP v2.0" +git commit -m "Pre-upgrade backup before HydePHP v3.0" ``` ### Estimated Time -The upgrade process typically takes **30-60 minutes** for most projects. Complex sites with extensive customizations may take up to 90 minutes. The majority of this time involves: - -- Updating and installing dependencies (~15 minutes) -- Migrating configuration files (~20 minutes) -- Testing and verifying the site (~15-30 minutes) +// ## Step 1: Update Dependencies ### Update Composer Dependencies -Open your `composer.json` file and update the following dependencies: - -```json -{ - "require": { - "php": "^8.2", - "hyde/framework": "^2.0", - "laravel-zero/framework": "^11.0" - }, - "require-dev": { - "hyde/realtime-compiler": "^4.0" - } -} -``` - -Then run: - -```bash -composer update -``` - -The dump-autoload script will likely fail, but this is expected and will be resolved in subsequent steps. +// ### Update Node Dependencies -Open your `package.json` file and replace the entire `devDependencies` section: - -**Before (v1.x with Laravel Mix):** -```json -{ - "devDependencies": { - "@tailwindcss/typography": "^0.5.2", - "autoprefixer": "^10.4.5", - "hydefront": "^3.4.1", - "laravel-mix": "^6.0.49", - "postcss": "^8.4.31", - "tailwindcss": "^3.0.24" - } -} -``` +HydePHP v3 upgrades the bundled `vite` dependency from v7 to v8. Update your `package.json` `devDependencies` to require the new major version: -**After (v2.x with Vite):** ```json { - "type": "module", "devDependencies": { - "@tailwindcss/typography": "^0.5.0", - "@tailwindcss/vite": "^4.1.0", - "autoprefixer": "^10.4.0", - "hyde-vite-plugin": "^1.1.0", - "hydefront": "^4.0.0", - "postcss": "^8.5.0", - "tailwindcss": "^4.1.0", - "vite": "^7.1.0" - } -} -``` - -Update the NPM scripts in your `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" + "vite": "^8.0.0" } } ``` -Then run: - -```bash -npm install -``` - -## Step 2: Migrate from Laravel Mix to Vite - -### Delete the Old Mix Configuration - -Remove the `webpack.mix.js` file from your project root. - -### Create the New Vite Configuration - -Create a new `vite.config.js` file in your project root: - -```javascript -import { defineConfig } from 'vite'; -import tailwindcss from "@tailwindcss/vite"; -import hyde from 'hyde-vite-plugin'; +Then run `npm install` (or your package manager's equivalent) to pick up the update. -export default defineConfig({ - plugins: [ - hyde({ - input: ['resources/assets/app.css', 'resources/assets/app.js'], - watch: ['_pages', '_posts', '_docs'], - refresh: true, - }), - tailwindcss(), - ], -}); -``` +If you have a custom `vite.config.js` that overrides `build.rollupOptions`, note that Vite 8 builds with Rolldown by default. The `hyde-vite-plugin` now configures its own build options under `build.rolldownOptions` rather than `build.rollupOptions` โ€” if your custom config only sets `rollupOptions`, double check your output still ends up where you expect after upgrading. -### Update Your CSS imports +## Step 2: Replace the Removed `rebuild` Command -Update `resources/assets/app.css`: +The `rebuild` command has been removed in v3.0. It had no remaining internal consumers now that the realtime compiler renders pages entirely in-memory, and building a single page could silently leave aggregate outputs (sitemap, RSS, search index, navigation) stale while looking like a complete build. **Before:** -```css -@import '~hydefront/dist/hyde.css'; - -@tailwind base; -@tailwind components; -@tailwind utilities; - -[x-cloak] { display: none !important; } -``` - -**After:** -```css -@import 'hydefront/components/torchlight.css' layer(base); - -@import 'tailwindcss'; - -@config '../../tailwind.config.js'; -``` - -## Step 3: Upgrade Tailwind CSS to v4 - -Run the automated Tailwind upgrade tool: - ```bash -npx @tailwindcss/upgrade -``` - -Review the [Tailwind v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) for detailed information about breaking changes in custom configurations. - -## Step 4: Verify Vite Works - -Now you can run Vite build: - -```bash -npm run build -``` - -## Step 5: Update Configuration Files - -### Update `config/hyde.php` - -#### Replace Features With Enum Values - -**Before:** -```php -'features' => [ - Features::htmlPages(), - Features::markdownPosts(), -], +php hyde rebuild _posts/hello-world.md ``` **After:** -```php -'features' => [ - Feature::HtmlPages, - Feature::MarkdownPosts, -], -``` -#### Update Navigation Configuration +If you need to build a single page programmatically, use `StaticPageBuilder::handle()` directly: -**Before:** ```php -'navigation' => [ - 'custom_items' => [ - 'Custom Item' => '/custom-page', - ], -], -``` +use Hyde\Foundation\Facades\Pages; +use Hyde\Framework\Actions\StaticPageBuilder; -**After:** -```php -'navigation' => [ - 'custom' => [ - ['label' => 'Custom Item', 'destination' => '/custom-page'], - ], -], +StaticPageBuilder::handle(Pages::getPage('_posts/hello-world.md')); ``` -Or use the Navigation facade: +Note that this only produces a correct `_site` when the page is self-contained. For anything that touches aggregate outputs, run `php hyde build` to rebuild the whole site instead. -```php -use Hyde\Facades\Navigation; +## Step 3: Replace the Removed Publishing Commands -'navigation' => [ - 'custom' => [ - Navigation::item('https://github.com/hydephp/hyde', 'GitHub', 200), - ], -], -``` +The three legacy publishing commands (`publish:views`, `publish:configs`, and `publish:homepage`) were removed in v3 and replaced by the unified `publish` command (and, for configuration files, the standard `vendor:publish` path). They are not aliased โ€” invoking one now raises the native "command not found" error, which already suggests `publish` as an alternative. -#### Update Subdirectory Display Setting +| Removed in v3 | Use instead | +|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| `publish:views` | `php hyde publish --all` (or `--layouts` / `--components`, or bare `publish` for the interactive picker) | +| `publish:views layouts` | `php hyde publish --layouts` | +| `publish:views components` | `php hyde publish --components` | +| `publish:configs` | `php hyde vendor:publish --tag=hyde-config --force` | +| `publish:homepage` | `php hyde publish --page` | +| `publish:homepage welcome` | `php hyde publish --page=welcome` | +| `publish:homepage posts` | `php hyde publish --page=posts --to=_pages/index.blade.php` (the old command always published to the index; the new default is `_pages/posts.blade.php`) | +| `publish:homepage blank` | `php hyde publish --page=blank --to=_pages/index.blade.php` (blank now has no default destination) | -**Before:** -```php -'navigation' => [ - 'subdirectories' => 'hidden', -], -``` +The config publish tags were consolidated too: `hyde-configs`, `support-configs`, and `configs` are removed, and `hyde-config` is now the only Hyde config publish tag. -**After:** -```php -'navigation' => [ - 'subdirectory_display' => 'hidden', -], -``` - - -### Update Author Configuration - -If you're using blog post authors, update the configuration format: - -**Before:** -```php -'authors' => [ - Author::create('username', 'Display Name', 'https://example.com'), -], -``` - -**After:** -```php -'authors' => [ - 'username' => Author::create( - name: 'Display Name', - website: 'https://example.com', - bio: 'Author bio', - avatar: 'avatar.png', - socials: ['twitter' => '@username'] - ), -], -``` - -#### Rename Cache Busting Setting - -**Before:** -```php -'enable_cache_busting' => true, -``` - -**After:** -```php -'cache_busting' => true, -``` - -#### Remove Deprecated HydeFront Settings - -Remove these configuration options (they're now handled automatically): - -```php -'hydefront_version' => ..., -'hydefront_cdn_url' => ..., -``` - -### Update `config/docs.php` - -Reorganize the sidebar configuration: - -**Before:** -```php -'sidebar_order' => [ - 'readme', - 'installation', -], - -'table_of_contents' => [ - 'enabled' => true, -], - -'sidebar_group_labels' => [ - // ... -], -``` - -**After:** -```php -'sidebar' => [ - 'order' => [ - 'readme', - 'installation', - ], - - 'labels' => [ - // ... - ], - - 'table_of_contents' => [ - 'enabled' => true, - 'min_heading_level' => 2, - 'max_heading_level' => 4, - ], -], -``` - -### Update `app/config.php` - -Add the new navigation service provider under the `'providers'` array: - -```php -Hyde\Foundation\Providers\NavigationServiceProvider::class, -``` - -And add the following classes to the `'aliases'` array: - -```php -'Vite' => \Hyde\Facades\Vite::class, -'MediaFile' => \Hyde\Support\Filesystem\MediaFile::class, -``` - -## Step 6: Update Code References - -### Routes Facade API Changes - -**Before:** -```php -$route = Routes::get('route-name'); // Returns null if not found -$route = Routes::getOrFail('route-name'); // Throws exception -``` - -**After:** -```php -$route = Routes::find('route-name'); // Returns null if not found -$route = Routes::get('route-name'); // Throws exception -``` - -### Asset API Updates - -If you're using asset methods in custom code, note that they now return `MediaFile` instances: - -```php -// Methods renamed for clarity -Hyde::asset('image.png'); // Previously: Hyde::mediaLink() -Asset::get('image.png'); // Previously: Asset::mediaLink() -Asset::exists('image.png'); // Previously: Asset::hasMediaFile() -HydeFront::cdnLink('app.css'); // Previously: Asset::cdnLink() -MediaFile::sourcePath('image.png') // Previously: Hyde::mediaPath() -``` - -The `MediaFile` instances are Stringable and will automatically resolve to relative links, so in most cases (especially in Blade templates), no code changes are needed. - -### Includes Facade Return Types - -Methods now return `HtmlString` objects instead of raw strings: - -**Before:** -```blade -{!! Includes::html('partial') !!} -``` - -**After:** -```blade -{{ Includes::html('partial') }} -``` - -**Security Note:** Output is no longer escaped by default. If you're including user-generated content, use `{{ e(Includes::html('foo')) }}` for escaping. - -### DataCollections Renamed - -**Before:** -```php -use Hyde\Support\DataCollections; -``` - -**After:** -```php -use Hyde\Support\DataCollection; -``` - -## Step 7: Update Build Commands - -Update any CI/CD pipelines or build scripts: - -**Before:** -```bash -npm run prod -php hyde build --run-prod -``` - -**After:** -```bash -npm run build -php hyde build --vite -``` - -The `--run-dev`, `--run-prod`, and `--run-prettier` flags have been removed. Use `--vite` instead. - -## Step 8: Clear Caches - -Next, to ensure we have a clean slate, run the following commands: - -```bash -composer dump-autoload -php hyde cache:clear -rm app/storage/framework/views/*.php -``` - -You may also want to republish any views you have published. - -## Step 9: Rebuild Your Site - -After completing all the configuration updates: - -```bash -npm run build - -# Build your site -php hyde build - -# Or use the realtime compiler for development -php hyde serve -``` - -## Step 10: Test Your Site - -1. Test all navigation menus for correct ordering and appearance -2. Verify media assets are loading correctly -3. Check that all pages render properly -4. Test the search functionality (if using documentation) -5. Verify author information displays correctly on blog posts -6. Test dark mode if you're using theme toggle buttons +Note that the new `publish` command never overwrites files you have modified without confirmation or `--force`, where the old commands overwrote silently. This is why the `publish:configs` replacement passes `--force` โ€” existing files are skipped without it. ## Migration Checklist Use this checklist to track your upgrade progress: -- [ ] Confirmed running HydePHP v1.6+ (preferably v1.8) -- [ ] Updated `composer.json` dependencies -- [ ] Ran `composer update` -- [ ] Updated `package.json` dependencies and scripts -- [ ] Added `"type": "module"` to `package.json` -- [ ] Ran `npm install` -- [ ] Deleted `webpack.mix.js` -- [ ] Created `vite.config.js` -- [ ] Updated `resources/assets/app.css` imports -- [ ] Ran `npx @tailwindcss/upgrade` for Tailwind v4 -- [ ] Updated `config/hyde.php` features to use enum values -- [ ] Updated navigation configuration to array format -- [ ] Renamed `enable_cache_busting` to `cache_busting` -- [ ] Removed deprecated HydeFront settings -- [ ] Updated author configuration format (if using blog) -- [ ] Reorganized sidebar configuration in `config/docs.php` -- [ ] Updated Routes facade method calls (if used in custom code) -- [ ] Updated Includes facade usage (if used in custom views) -- [ ] Renamed DataCollections to DataCollection (if used) -- [ ] Ran `npm run build` -- [ ] Ran `php hyde build` -- [ ] Tested site menus for correct ordering and appearance -- [ ] Verified media assets load correctly -- [ ] Checked all pages render properly -- [ ] Tested documentation search (if applicable) -- [ ] Verified blog author information (if applicable) +- [ ] Replaced any `php hyde rebuild ` usage with `StaticPageBuilder::handle()` or a full `php hyde build` +- [ ] Replaced any `publish:views`, `publish:configs`, or `publish:homepage` usage with the new `publish` / `vendor:publish` commands (see the table above; note the `posts`/`blank` `--to` mappings) ## Troubleshooting -### Assets Not Compiling - -If you encounter issues with asset compilation: - -1. Delete `node_modules` and `package-lock.json` -2. Run `npm install` again -3. Clear the `_media` directory -4. Run `npm run build` - -### Missing Routes - -If you get `RouteNotFoundException` errors: - -- Check that you've updated `Routes::get()` to `Routes::find()` for cases where the route might not exist -- Verify your page source files are in the correct directories - -### Validation Errors - -If you get syntax validation errors from DataCollection: - -- Ensure all your YAML/JSON data files have valid syntax -- Empty data files are no longer allowed in v2.0 - -### Errors During build - -If you have published Blade views, those may need to be republished if they use old syntax. If you use custom code you may need to look closer at the full release diff. ## Getting Help If you encounter issues during the upgrade: -- **Documentation**: [https://hydephp.com/docs/2.x](https://hydephp.com/docs/2.x) +- **Documentation**: [https://hydephp.com/docs/3.x](https://hydephp.com/docs/3.x) - **GitHub Issues**: [https://github.com/hydephp/hyde/issues](https://github.com/hydephp/hyde/issues) - **Community Discord**: [https://discord.hydephp.com](https://discord.hydephp.com) -For the complete changelog with all pull request references, see the [full release notes](https://github.com/hydephp/hyde/releases/tag/v2.0.0). +For the complete changelog with all pull request references, see the [full release notes](https://github.com/hydephp/hyde/releases/tag/v3.0.0). diff --git a/_pages/index.blade.php b/_pages/index.blade.php index ed3a8c6175a..dfbdf321fad 100644 --- a/_pages/index.blade.php +++ b/_pages/index.blade.php @@ -52,7 +52,7 @@ class="relative mt-2 text-transparent bg-clip-text bg-linear-to-br logo-gradient This is the default homepage stored as index.blade.php, however you can publish any of the built-in views using the following command: -
php hyde publish:homepage
+
php hyde publish --page

diff --git a/docs/creating-content/compile-and-deploy.md b/docs/creating-content/compile-and-deploy.md index 7025c3ba559..d8c23b6cfd0 100644 --- a/docs/creating-content/compile-and-deploy.md +++ b/docs/creating-content/compile-and-deploy.md @@ -16,12 +16,6 @@ Now that you have some amazing content, you'll want to compile your site into st php hyde build ``` -**You can also compile a single file, though this is deprecated and will be removed in v3.0 (use `Hyde\Framework\Actions\StaticPageBuilder::handle()` instead if you need this programmatically):** - -```bash -php hyde rebuild -``` - **And, you can even start a development server to compile your site on the fly:** ```bash @@ -34,9 +28,8 @@ php hyde serve #### Learn more about these commands in the [console commands](console-commands) documentation: -- [Build command](console-commands#build-the-static-site) -- [Rebuild command (Deprecated)](console-commands#rebuild) -- [Serve command](console-commands#start-the-realtime-compiler) +- [Build command](console-commands#build) +- [Serve command](console-commands#serve) --- diff --git a/docs/creating-content/managing-assets.md b/docs/creating-content/managing-assets.md index 97444410c16..3848770d86b 100644 --- a/docs/creating-content/managing-assets.md +++ b/docs/creating-content/managing-assets.md @@ -130,7 +130,7 @@ To make it really easy to customize asset loading, the styles and scripts are lo To customize them, run the following command: ```bash -php hyde publish:views layouts +php hyde publish --layouts ``` Then edit the files found in `resources/views/vendor/hyde/layouts` directory of your project. diff --git a/docs/digging-deeper/advanced-markdown.md b/docs/digging-deeper/advanced-markdown.md index d74b2842093..c034418a3ef 100644 --- a/docs/digging-deeper/advanced-markdown.md +++ b/docs/digging-deeper/advanced-markdown.md @@ -70,7 +70,7 @@ coloured blockquotes. Simply append the desired colour after the initial `>` cha You can easily customize these styles by publishing and editing the `markdown-blockquote.blade.php` file. ```bash -php hyde publish:views components +php hyde publish --components ``` ### Markdown usage @@ -166,7 +166,7 @@ You can enable it for other page types by adding the page class to the `permalin Under the hood, Hyde uses a custom Blade-based heading renderer when converting Markdown to HTML. This allows for more flexibility and customization compared to standard Markdown parsers. You can also publish and customize the Blade component used to render the headings: ```bash -php hyde publish:components +php hyde publish --components ``` This will copy the `markdown-heading.blade.php` component to your views directory where you can modify its markup and behavior. diff --git a/docs/digging-deeper/customization.md b/docs/digging-deeper/customization.md index d52350726af..0c6f81e0bdf 100644 --- a/docs/digging-deeper/customization.md +++ b/docs/digging-deeper/customization.md @@ -81,7 +81,7 @@ The main configuration file, `hyde.php`, is used for things ranging from site na Since HydePHP is based on Laravel we also have a few configuration files related to them. As you most often don't need to edit any of these, unless you want to make changes to the underlying application, they are not present in the -base HydePHP installation. However, you can publish them to your project by running `php hyde publish:configs`. +base HydePHP installation. However, you can publish them to your project by running `php hyde vendor:publish --tag=hyde-config`. | Config File | Description | |------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| @@ -92,7 +92,7 @@ base HydePHP installation. However, you can publish them to your project by runn {.align-top} -If any of these files are missing, you can run `php hyde publish:configs` to copy the default files to your project. +If any of these files are missing, you can run `php hyde vendor:publish --tag=hyde-config` to copy the default files to your project. ## Configuration Options @@ -371,12 +371,15 @@ instead the appropriate color scheme will be automatically applied based on the Hyde uses the Laravel Blade templating engine. Most parts of the included templates have been extracted into components to be customized easily. Before editing the views you should familiarize yourself with the [Laravel Blade Documentation](https://laravel.com/docs/10.x/blade). -To edit a default Hyde component you need to publish them first using the `hyde publish:views` command. +To edit a default Hyde component you need to publish them first using the `hyde publish` command. ```bash -php hyde publish:views +php hyde publish --all ``` +You can also run `php hyde publish` without any flags to interactively pick which views to publish, +or scope the command to just `--layouts` or `--components`. + The files will then be available in the `resources/views/vendor/hyde` directory. >info **Tip:** If you use Linux/macOS or Windows with WSL you will be able to interactively select individual files to publish. diff --git a/docs/digging-deeper/helpers.md b/docs/digging-deeper/helpers.md index 5ebdfde700a..9c375272d51 100644 --- a/docs/digging-deeper/helpers.md +++ b/docs/digging-deeper/helpers.md @@ -333,8 +333,8 @@ HydePHP comes with a started homepage called 'posts'. This includes a component #### Creating our posts page -Now, let's paginate this feed! For this example, we will assume that you ran `php hyde publish:homepage posts` -and renamed the resulting `index.blade.php` file to `posts.blade.php`. We will also assume that you have a few blog posts set up. +Now, let's paginate this feed! For this example, we will assume that you ran `php hyde publish --page=posts` +to publish the posts feed page to `_pages/posts.blade.php`. We will also assume that you have a few blog posts set up. The blog post feed component is a simple component that looks like this: diff --git a/docs/digging-deeper/troubleshooting.md b/docs/digging-deeper/troubleshooting.md index a999358651d..03a63daeea1 100644 --- a/docs/digging-deeper/troubleshooting.md +++ b/docs/digging-deeper/troubleshooting.md @@ -76,13 +76,13 @@ You can read more about some of these in the [Core Concepts](core-concepts#paths | Wrong layout used | Hyde determines the layout template to use depending on the directory of the source file | Ensure your source file is in the right directory. | | Invalid/no permalinks or post URIs | You may be missing or have an invalid site URL | Set the site URL in the `.env` file | | No styles in custom Blade pages | When using custom blade pages need to add the styles yourself. You can do this by extending the default layout | Use the app layout, or by include the Blade components directly. | -| Overriding Hyde views is not working | Ensure the Blade views are in the correct directory. | Rerun `php hyde publish:views`. | +| Overriding Hyde views is not working | Ensure the Blade views are in the correct directory. | Rerun `php hyde publish --all`. | | Styles not updating when deploying site | It could be a caching issue. To be honest, when dealing with styles, it's always a caching issue. | Clear your cache, and optionally complain to your site host | | Documentation sidebar items are in the wrong order | Double check the config, make sure the route keys are written correctly. Check that you are not overriding with front matter. | Check config for typos and front matter | | Issues with date in blog post front matter | The date is parsed by the PHP `strtotime()` function. The date may be in an invalid format, or the front matter is invalid | Ensure the date is in a format that `strtotime()` can parse. Wrap the front matter value in quotes. | | RSS feed not being generated | The RSS feed requires that you have set a site URL in the Hyde config or the `.env` file. Also check that you have blog posts, and that they are enabled. | Check your configuration files. | | | Sitemap not being generated | The sitemap requires that you have set a site URL in the Hyde config or the `.env` file. | Check your configuration files. | | -| Unable to do literally anything | If everything is broken, you may be missing a Composer package or your configuration files could be messed up. | Run `composer install` and/or `composer update`. If you can run HydeCLI commands, update your configs with `php hyde publish:configs`, or copy them manually from GitHub or the vendor directory. | +| Unable to do literally anything | If everything is broken, you may be missing a Composer package or your configuration files could be messed up. | Run `composer install` and/or `composer update`. If you can run HydeCLI commands, update your configs with `php hyde vendor:publish --tag=hyde-config --force` (existing files are skipped without `--force`), or copy them manually from GitHub or the vendor directory. | | Namespaced Yaml config (`hyde.yml`) not working | When using namespaced Yaml configuration, you must begin the file with `hyde:`, even if you just want to use another file for example `docs:`. | Make sure the file starts with `hyde:` (You don't need to specify any options, as long as it's present). See [`#1475`](https://github.com/hydephp/develop/issues/1475) | ### Extra troubleshooting information diff --git a/docs/digging-deeper/updating-hyde.md b/docs/digging-deeper/updating-hyde.md index 603f3368a21..ba27ba45c90 100644 --- a/docs/digging-deeper/updating-hyde.md +++ b/docs/digging-deeper/updating-hyde.md @@ -89,14 +89,16 @@ composer update Then, update your config files. This is the hardest part, as you may need to manually copy in your own changes. ```bash -php hyde publish:configs +php hyde vendor:publish --tag=hyde-config --force ``` +Note that existing files are skipped without `--force`, so it is required here to overwrite your current config files. + If you have published any of the included Blade components you will need to re-publish them. ```bash -php hyde publish:views layouts -php hyde publish:views components +php hyde publish --layouts +php hyde publish --components ``` You may also want to download any resources that have been updated. You download these from the Zip file of the latest release on GitHub. diff --git a/docs/getting-started/console-commands.md b/docs/getting-started/console-commands.md index ac6bd345847..2a1b66fbdfb 100644 --- a/docs/getting-started/console-commands.md +++ b/docs/getting-started/console-commands.md @@ -47,16 +47,13 @@ Here is a quick reference of all the available commands. You can also run `php h |-----------------------------------------|---------------------------------------------------------------------------------------------| | [`build`](#build) | Build the static site | | [`serve`](#serve) | Start the realtime compiler server | -| [`rebuild`](#rebuild) | Run the static site builder for a single file (Deprecated, will be removed in v3.0) | | [`build:rss`](#build-rss) | Generate the RSS feed | | [`build:search`](#build-search) | Generate the `docs/search.json` file | | [`build:sitemap`](#build-sitemap) | Generate the `sitemap.xml` file | | [`make:page`](#make-page) | Scaffold a new Markdown, Blade, or documentation page file | | [`make:post`](#make-post) | Scaffold a new Markdown blog post file | -| [`publish:configs`](#publish-configs) | Publish the default configuration files | -| [`publish:homepage`](#publish-homepage) | Publish one of the default homepages as `index.blade.php` | -| [`publish:views`](#publish-views) | Publish the hyde components for customization. Note that existing files will be overwritten | -| [`vendor:publish`](#vendor-publish) | Publish any publishable assets from vendor packages | +| [`publish`](#publish) | Publish Hyde views and starter pages for customization | +| [`vendor:publish`](#vendor-publish) | Publish any publishable assets from vendor packages (including the Hyde config files) | | [`route:list`](#route-list) | Display all registered routes | | [`validate`](#validate) | Run a series of tests to validate your setup and help you optimize your site | | [`list`](#available-commands) | List all available commands | @@ -79,24 +76,6 @@ Build the static site | `--pretty-urls` | Should links in output use pretty URLs? | | `--no-api` | Disable API calls, for example, Torchlight | -## Run the static site builder for a single file - - - ->warning **Deprecated:** The `rebuild` command is deprecated and will be removed in HydePHP v3.0. It has no remaining internal consumers now that the realtime compiler renders pages in-memory, and building a single page can silently leave aggregate outputs (sitemap, RSS, search index, navigation) stale. If you need to build a single page programmatically, use `Hyde\Framework\Actions\StaticPageBuilder::handle()` instead. - -```bash -php hyde rebuild -``` - -Run the static site builder for a single file - -#### Arguments - -| | | -|--------|--------------------------------------------------------------------------------| -| `path` | The relative file path (example: \_posts/hello-world.md) \n - Is required: yes | - ## Start the Realtime Compiler Server @@ -191,48 +170,35 @@ Scaffold a new Markdown blog post file | `title` | The title for the Post. Will also be used to generate the filename | | `--force` | Should the generated file overwrite existing posts with the same filename? | -## Publish the Default Configuration Files - - - -```bash -php hyde publish:configs -``` - -Publish the default configuration files +## Publish Hyde views and starter pages for customization -## Publish one of the default homepages as `index.blade.php`. - - + ```bash -php hyde publish:homepage [--force] [--] [] +php hyde publish [--layouts] [--components] [--all] [--page[=NAME]] [--to=PATH] [--force] ``` -Publish one of the default homepages as `index.blade.php`. - -#### Arguments & Options - -| | | -|------------|---------------------------------| -| `homepage` | The name of the page to publish | -| `--force` | Overwrite any existing files | +Publish Hyde views and starter pages for customization. -## Publish the hyde components for customization +With no flags, `publish` runs an interactive wizard that lets you choose between publishing +**views** (Hyde's Blade layouts and components) or **a starter page** (such as a homepage or 404 page). +Each flag simply skips a step of the wizard. Existing files that you have modified are never overwritten +without your confirmation or the `--force` flag. - - -```bash -php hyde publish:views [] -``` +#### Options -Publish the hyde components for customization. Note that existing files will be overwritten. +| Option | Description | +|----------------|--------------------------------------------------------------| +| `--layouts` | Scope publishing to the Hyde layout views | +| `--components` | Scope publishing to the Hyde component views | +| `--all` | Publish all Hyde views without the picker | +| `--page[=NAME]`| Publish a starter page, optionally by name (e.g. `--page=welcome`) | +| `--to=PATH` | Destination path for a published page (pages only) | +| `--force` | Overwrite files that you have modified | -#### Arguments +Published views land in `resources/views/vendor/hyde`, and published pages land in `_pages`. -| | | -|----------|-------------------------| -| `group` | The group to publish | +>info **Tip:** To publish the Hyde configuration files, use `php hyde vendor:publish --tag=hyde-config`. See the [`vendor:publish`](#vendor-publish) command below. ## Display All Registered Routes. @@ -252,7 +218,14 @@ Display all registered routes. php hyde vendor:publish [--existing] [--force] [--all] [--provider [PROVIDER]] [--tag [TAG]] ``` -Publish any publishable assets from vendor packages +Publish any publishable assets from vendor packages. This is the advanced Laravel publishing path, +and is also where the Hyde configuration files are published from, using the `hyde-config` tag: + +```bash +php hyde vendor:publish --tag=hyde-config +``` + +This publishes the Hyde-owned config files (`hyde.php`, `docs.php`, `markdown.php`, `view.php`, `cache.php`, and `commands.php`) to your project's `config` directory. #### Options @@ -263,3 +236,7 @@ Publish any publishable assets from vendor packages | `--all` | Publish assets for all service providers without prompt | | `--provider=` | The service provider that has assets you want to publish | | `--tag=` | One or many tags that have assets you want to publish \n- Is multiple: yes | + +## Removed publishing commands + +The legacy `publish:*` publishing commands from earlier versions of Hyde were removed in v3. See the [Removed Publishing Commands](https://hydephp.com/docs/3.x/upgrade-guide#removed-publishing-commands) section of the upgrade guide for the replacement for each. diff --git a/docs/getting-started/upgrade-guide.md b/docs/getting-started/upgrade-guide.md index 70896bc5978..34432bb27a1 100644 --- a/docs/getting-started/upgrade-guide.md +++ b/docs/getting-started/upgrade-guide.md @@ -1,544 +1,33 @@ --- navigation: - label: v2 Upgrade Guide + label: v3 Upgrade Guide priority: 16 --- -# HydePHP v2.0 Upgrade Guide +Will be filled in from UPGRADE.md before release. -## Overview +## Removed Publishing Commands -HydePHP v2.0 represents a major evolution of the framework, introducing significant improvements to the asset system, navigation API, and overall developer experience. This release modernizes the frontend tooling by replacing Laravel Mix with Vite, completely rewrites the navigation system for better flexibility, and introduces numerous performance optimizations throughout the framework. +The three legacy publishing commands were removed in v3 and replaced by the unified `publish` command +(and, for configuration files, the standard `vendor:publish` path). They are not aliased โ€” invoking one +now raises the native "command not found" error, which already suggests `publish` as an alternative. -This document will guide you through the upgrade process. If you want to learn more about the new features, take a look at the [Release Notes](https://hydephp.com/docs/2.x/release-notes). + -## Before You Begin +| Removed in v3 | Use instead | +|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| `publish:views` | `php hyde publish --all` (or `--layouts` / `--components`, or bare `publish` for the interactive picker) | +| `publish:views layouts` | `php hyde publish --layouts` | +| `publish:views components`| `php hyde publish --components` | +| `publish:configs` | `php hyde vendor:publish --tag=hyde-config --force` | +| `publish:homepage` | `php hyde publish --page` | +| `publish:homepage welcome`| `php hyde publish --page=welcome` | +| `publish:homepage posts` | `php hyde publish --page=posts --to=_pages/index.blade.php` (the old command always published to the index; the new default is `_pages/posts.blade.php`) | +| `publish:homepage blank` | `php hyde publish --page=blank --to=_pages/index.blade.php` (blank now has no default destination) | -### Prerequisites +The config publish tags were consolidated too: `hyde-configs`, `support-configs`, and `configs` are removed, +and `hyde-config` is now the only Hyde config publish tag. -Before upgrading to HydePHP v2.0, ensure your application is running **HydePHP v1.6 or later** (ideally v1.8). These versions include transition helpers that will make the migration process smoother. - -### Backup Your Project - -Before starting the upgrade process, it's **strongly recommended** to: - -- **Commit all changes to Git** - This allows you to easily revert if needed -- **Create a backup** of your entire project directory -- **Have a previous site build** so you can compare output - -If you're not already using Git for version control, now is an excellent time to initialize a repository: - -```bash -git init -git add . -git commit -m "Pre-upgrade backup before HydePHP v2.0" -``` - -### Estimated Time - -The upgrade process typically takes **30-60 minutes** for most projects. Complex sites with extensive customizations may take up to 90 minutes. The majority of this time involves: - -- Updating and installing dependencies (~15 minutes) -- Migrating configuration files (~20 minutes) -- Testing and verifying the site (~15-30 minutes) - -## Step 1: Update Dependencies - -### Update Composer Dependencies - -Open your `composer.json` file and update the following dependencies: - -```json -{ - "require": { - "php": "^8.2", - "hyde/framework": "^2.0", - "laravel-zero/framework": "^11.0" - }, - "require-dev": { - "hyde/realtime-compiler": "^4.0" - } -} -``` - -Then run: - -```bash -composer update -``` - -The dump-autoload script will likely fail, but this is expected and will be resolved in subsequent steps. - -### Update Node Dependencies - -Open your `package.json` file and replace the entire `devDependencies` section: - -**Before (v1.x with Laravel Mix):** -```json -{ - "devDependencies": { - "@tailwindcss/typography": "^0.5.2", - "autoprefixer": "^10.4.5", - "hydefront": "^3.4.1", - "laravel-mix": "^6.0.49", - "postcss": "^8.4.31", - "tailwindcss": "^3.0.24" - } -} -``` - -**After (v2.x with Vite):** -```json -{ - "type": "module", - "devDependencies": { - "@tailwindcss/typography": "^0.5.0", - "@tailwindcss/vite": "^4.1.0", - "autoprefixer": "^10.4.0", - "hyde-vite-plugin": "^1.1.0", - "hydefront": "^4.0.0", - "postcss": "^8.5.0", - "tailwindcss": "^4.1.0", - "vite": "^7.1.0" - } -} -``` - -Update the NPM scripts in your `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Then run: - -```bash -npm install -``` - -## Step 2: Migrate from Laravel Mix to Vite - -### Delete the Old Mix Configuration - -Remove the `webpack.mix.js` file from your project root. - -### Create the New Vite Configuration - -Create a new `vite.config.js` file in your project root: - -```javascript -import { defineConfig } from 'vite'; -import tailwindcss from "@tailwindcss/vite"; -import hyde from 'hyde-vite-plugin'; - -export default defineConfig({ - plugins: [ - hyde({ - input: ['resources/assets/app.css', 'resources/assets/app.js'], - watch: ['_pages', '_posts', '_docs'], - refresh: true, - }), - tailwindcss(), - ], -}); -``` - -### Update Your CSS imports - -Update `resources/assets/app.css`: - -**Before:** -```css -@import '~hydefront/dist/hyde.css'; - -@tailwind base; -@tailwind components; -@tailwind utilities; - -[x-cloak] { display: none !important; } -``` - -**After:** -```css -@import 'hydefront/components/torchlight.css' layer(base); - -@import 'tailwindcss'; - -@config '../../tailwind.config.js'; -``` - -## Step 3: Upgrade Tailwind CSS to v4 - -Run the automated Tailwind upgrade tool: - -```bash -npx @tailwindcss/upgrade -``` - -Review the [Tailwind v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) for detailed information about breaking changes in custom configurations. - -## Step 4: Verify Vite Works - -Now you can run Vite build: - -```bash -npm run build -``` - -## Step 5: Update Configuration Files - -### Update `config/hyde.php` - -#### Replace Features With Enum Values - -**Before:** -```php -'features' => [ - Features::htmlPages(), - Features::markdownPosts(), -], -``` - -**After:** -```php -'features' => [ - Feature::HtmlPages, - Feature::MarkdownPosts, -], -``` - -#### Update Navigation Configuration - -**Before:** -```php -'navigation' => [ - 'custom_items' => [ - 'Custom Item' => '/custom-page', - ], -], -``` - -**After:** -```php -'navigation' => [ - 'custom' => [ - ['label' => 'Custom Item', 'destination' => '/custom-page'], - ], -], -``` - -Or use the Navigation facade: - -```php -use Hyde\Facades\Navigation; - -'navigation' => [ - 'custom' => [ - Navigation::item('https://github.com/hydephp/hyde', 'GitHub', 200), - ], -], -``` - -#### Update Subdirectory Display Setting - -**Before:** -```php -'navigation' => [ - 'subdirectories' => 'hidden', -], -``` - -**After:** -```php -'navigation' => [ - 'subdirectory_display' => 'hidden', -], -``` - - -### Update Author Configuration - -If you're using blog post authors, update the configuration format: - -**Before:** -```php -'authors' => [ - Author::create('username', 'Display Name', 'https://example.com'), -], -``` - -**After:** -```php -'authors' => [ - 'username' => Author::create( - name: 'Display Name', - website: 'https://example.com', - bio: 'Author bio', - avatar: 'avatar.png', - socials: ['twitter' => '@username'] - ), -], -``` - -#### Rename Cache Busting Setting - -**Before:** -```php -'enable_cache_busting' => true, -``` - -**After:** -```php -'cache_busting' => true, -``` - -#### Remove Deprecated HydeFront Settings - -Remove these configuration options (they're now handled automatically): - -```php -'hydefront_version' => ..., -'hydefront_cdn_url' => ..., -``` - -### Update `config/docs.php` - -Reorganize the sidebar configuration: - -**Before:** -```php -'sidebar_order' => [ - 'readme', - 'installation', -], - -'table_of_contents' => [ - 'enabled' => true, -], - -'sidebar_group_labels' => [ - // ... -], -``` - -**After:** -```php -'sidebar' => [ - 'order' => [ - 'readme', - 'installation', - ], - - 'labels' => [ - // ... - ], - - 'table_of_contents' => [ - 'enabled' => true, - 'min_heading_level' => 2, - 'max_heading_level' => 4, - ], -], -``` - -### Update `app/config.php` - -Add the new navigation service provider under the `'providers'` array: - -```php -Hyde\Foundation\Providers\NavigationServiceProvider::class, -``` - -And add the following classes to the `'aliases'` array: - -```php -'Vite' => \Hyde\Facades\Vite::class, -'MediaFile' => \Hyde\Support\Filesystem\MediaFile::class, -``` - -## Step 6: Update Code References - -### Routes Facade API Changes - -**Before:** -```php -$route = Routes::get('route-name'); // Returns null if not found -$route = Routes::getOrFail('route-name'); // Throws exception -``` - -**After:** -```php -$route = Routes::find('route-name'); // Returns null if not found -$route = Routes::get('route-name'); // Throws exception -``` - -### Asset API Updates - -If you're using asset methods in custom code, note that they now return `MediaFile` instances: - -```php -// Methods renamed for clarity -Hyde::asset('image.png'); // Previously: Hyde::mediaLink() -Asset::get('image.png'); // Previously: Asset::mediaLink() -Asset::exists('image.png'); // Previously: Asset::hasMediaFile() -HydeFront::cdnLink('app.css'); // Previously: Asset::cdnLink() -MediaFile::sourcePath('image.png') // Previously: Hyde::mediaPath() -``` - -The `MediaFile` instances are Stringable and will automatically resolve to relative links, so in most cases (especially in Blade templates), no code changes are needed. - -### Includes Facade Return Types - -Methods now return `HtmlString` objects instead of raw strings: - -**Before:** -```blade -{!! Includes::html('partial') !!} -``` - -**After:** -```blade -{{ Includes::html('partial') }} -``` - -**Security Note:** Output is no longer escaped by default. If you're including user-generated content, use `{{ e(Includes::html('foo')) }}` for escaping. - -### DataCollections Renamed - -**Before:** -```php -use Hyde\Support\DataCollections; -``` - -**After:** -```php -use Hyde\Support\DataCollection; -``` - -## Step 7: Update Build Commands - -Update any CI/CD pipelines or build scripts: - -**Before:** -```bash -npm run prod -php hyde build --run-prod -``` - -**After:** -```bash -npm run build -php hyde build --vite -``` - -The `--run-dev`, `--run-prod`, and `--run-prettier` flags have been removed. Use `--vite` instead. - -## Step 8: Clear Caches - -Next, to ensure we have a clean slate, run the following commands: - -```bash -composer dump-autoload -php hyde cache:clear -rm app/storage/framework/views/*.php -``` - -You may also want to republish any views you have published. - -## Step 9: Rebuild Your Site - -After completing all the configuration updates: - -```bash -npm run build - -# Build your site -php hyde build - -# Or use the realtime compiler for development -php hyde serve -``` - -## Step 10: Test Your Site - -1. Test all navigation menus for correct ordering and appearance -2. Verify media assets are loading correctly -3. Check that all pages render properly -4. Test the search functionality (if using documentation) -5. Verify author information displays correctly on blog posts -6. Test dark mode if you're using theme toggle buttons - -## Migration Checklist - -Use this checklist to track your upgrade progress: - -- [ ] Confirmed running HydePHP v1.6+ (preferably v1.8) -- [ ] Updated `composer.json` dependencies -- [ ] Ran `composer update` -- [ ] Updated `package.json` dependencies and scripts -- [ ] Added `"type": "module"` to `package.json` -- [ ] Ran `npm install` -- [ ] Deleted `webpack.mix.js` -- [ ] Created `vite.config.js` -- [ ] Updated `resources/assets/app.css` imports -- [ ] Ran `npx @tailwindcss/upgrade` for Tailwind v4 -- [ ] Updated `config/hyde.php` features to use enum values -- [ ] Updated navigation configuration to array format -- [ ] Renamed `enable_cache_busting` to `cache_busting` -- [ ] Removed deprecated HydeFront settings -- [ ] Updated author configuration format (if using blog) -- [ ] Reorganized sidebar configuration in `config/docs.php` -- [ ] Updated Routes facade method calls (if used in custom code) -- [ ] Updated Includes facade usage (if used in custom views) -- [ ] Renamed DataCollections to DataCollection (if used) -- [ ] Ran `npm run build` -- [ ] Ran `php hyde build` -- [ ] Tested site menus for correct ordering and appearance -- [ ] Verified media assets load correctly -- [ ] Checked all pages render properly -- [ ] Tested documentation search (if applicable) -- [ ] Verified blog author information (if applicable) - -## Troubleshooting - -### Assets Not Compiling - -If you encounter issues with asset compilation: - -1. Delete `node_modules` and `package-lock.json` -2. Run `npm install` again -3. Clear the `_media` directory -4. Run `npm run build` - -### Missing Routes - -If you get `RouteNotFoundException` errors: - -- Check that you've updated `Routes::get()` to `Routes::find()` for cases where the route might not exist -- Verify your page source files are in the correct directories - -### Validation Errors - -If you get syntax validation errors from DataCollection: - -- Ensure all your YAML/JSON data files have valid syntax -- Empty data files are no longer allowed in v2.0 - -### Errors During build - -If you have published Blade views, those may need to be republished if they use old syntax. If you use custom code you may need to look closer at the full release diff. - -## Getting Help - -If you encounter issues during the upgrade: - -- **Documentation**: [https://hydephp.com/docs/2.x](https://hydephp.com/docs/2.x) -- **GitHub Issues**: [https://github.com/hydephp/hyde/issues](https://github.com/hydephp/hyde/issues) -- **Community Discord**: [https://discord.hydephp.com](https://discord.hydephp.com) - -For the complete changelog with all pull request references, see the [full release notes](https://github.com/hydephp/hyde/releases/tag/v2.0.0). +>info **Behavioral note:** The new `publish` command never overwrites files you have modified without confirmation +> or `--force`, where the old commands overwrote silently. This is the improvement the break buys โ€” and it is why +> the `publish:configs` replacement above passes `--force` (existing files are skipped without it). diff --git a/new-publish-command-implementation-plan.md b/new-publish-command-implementation-plan.md new file mode 100644 index 00000000000..31e61d4ea7f --- /dev/null +++ b/new-publish-command-implementation-plan.md @@ -0,0 +1,171 @@ +# HydePHP v3 โ€” `publish` Implementation Plan + +Companion to `hyde-v3-publish-spec.md`. Drive this **one step at a time**. +Hand the agent a single step + the spec โ€” never the whole plan. + +--- + +## Rules of engagement (paste as standing instructions to the agent) + +1. **One step only.** You are working on the single step I give you. Do not start, + scaffold, or "prepare for" any other step. +2. **Plan before code.** First restate the step's scope in your own words and list + the exact files you will create or edit. Wait for my approval before writing code. +3. **Stay in the box.** Touch only the files listed for this step. No unrelated + refactors, renames, reformatting, or "while I'm here" changes. If you believe + another file must change, STOP and ask โ€” don't do it. +4. **Implement to the spec, not to invention.** Follow the referenced spec section + exactly. Do not add flags, commands, names, options, or behaviors that aren't in + the spec. If the spec is ambiguous or seems wrong, STOP and ask โ€” don't guess. +5. **Green means done.** The step is complete only when: (a) tests pinning this + step's acceptance checks pass, and (b) the existing suite still passes. Use the + project's existing test framework and conventions. +6. **One commit per step**, message referencing the step number. I review the diff + before we move on. +7. If I say "out of scope for this step," drop it without argument. + +### Per-step prompt template + +> Implement **Step N** from the implementation plan (pasted below), following the +> spec section it references (pasted below). Obey the rules of engagement. +> Start by restating the scope and listing the files you'll touch, then wait for me. +> +> [paste the step block] +> [paste the referenced spec section] + +--- + +## Step 1 โ€” `OverwritePolicy` service + +- **Goal:** the shared, pure decision logic for whether to copy/skip/protect a file. +- **In scope:** a service that, given a source path and destination path, returns + one of `copy` (missing), `skip` (byte-identical), `blocked` (exists & differs). + No console UI. No knowledge of views/pages. +- **Out of scope:** any picker, any command wiring, any checksum manifest / historical + version detection (explicitly excluded by the spec). +- **Files:** the new policy service class + its unit test. Nothing else. +- **Depends on:** nothing. +- **Done when:** unit tests cover all three states (missing / identical / modified), + and the suite is green. +- **Spec ref:** ยง7. + +## Step 2 โ€” `PublishablePage` value object + `PublishablePages` registry + +- **Goal:** the data model for starter pages. +- **In scope:** the immutable `PublishablePage` value object (ยง5.1 shape), the + `PublishablePages` registry (`all()`, `get()`, `register()`), and the initial + catalog from ยง5.2 (welcome / posts / blank / 404) registered as defaults. +- **Out of scope:** destination resolution logic, any command or picker, any writing + of files. This step is data + registry only. +- **Files:** the value object, the registry, the catalog registration, and their tests. +- **Depends on:** nothing. +- **Done when:** tests assert the catalog contents (keys, default targets, + alternatives, `allowCustomTarget`) and that `register()` adds a page. Suite green. +- **Spec ref:** ยง5.1, ยง5.2. + +## Step 3 โ€” `PublishCommand` spine (flags, guardrails, wizard routing to stubs) + +- **Goal:** the command shell with the full flag surface, all error/guardrail + behavior, and the interactive wizard step 1 โ€” routing to **stub** handlers. +- **In scope:** register `php hyde publish`; define `--layouts --components --all + --page[=NAME] --to=PATH --force`; implement ยง2 guardrails (reject `--tag`/`--provider` + with a redirect message; `--layouts` + `--components` mutually exclusive; `--to` + only with `--page`; `--config` โ†’ redirect to `vendor:publish --tag=hyde-config`; + non-interactive with no actionable flags โ†’ usage error); implement the ยง3 wizard + step-1 menu routing to two stub methods (`publishViews()`, `publishPage()`) that + currently just print "not yet implemented". +- **Out of scope:** real views logic, real pages logic, real config tag. Handlers are + stubs. Do not implement OverwritePolicy calls yet. +- **Files:** the `PublishCommand` class, its service-provider registration, and the + command's guardrail/routing tests. +- **Depends on:** nothing (stubs stand in for Steps 4โ€“5). +- **Done when:** tests cover every guardrail/redirect in ยง9 and the wizard routing, + against the stubbed handlers. Suite green. +- **Spec ref:** ยง2, ยง3, ยง9. + +## Step 4 โ€” Views flow + +- **Goal:** replace the `publishViews()` stub with the real views publisher. +- **In scope:** `ViewsPublisher` reading the declared `layouts` / `components` groups; + the single grouped multi-select picker (ยง4) with an "All views" option; + `--layouts` / `--components` prefiltering; `--all` skipping the picker; + cardinality-aware output; wiring `OverwritePolicy` + `--force` and the interactive + conflict prompt / non-interactive `--force` error from ยง7. +- **Out of scope:** anything pages, config, or deprecation-related. Don't touch the + page stub. +- **Files:** `ViewsPublisher` (+ any small multiselect helper), the group declarations, + edits to `PublishCommand`'s `publishViews()` only, and views-flow tests. +- **Depends on:** Steps 1, 3. +- **Done when:** tests cover single/many/all selection, both group prefilters, + cardinality-aware output strings, and the overwrite/force behavior. Suite green. +- **Spec ref:** ยง4, ยง7. + +## Step 5 โ€” Pages flow + +- **Goal:** replace the `publishPage()` stub with the real pages publisher. +- **In scope:** `PagesPublisher` using the registry; destination resolution precedence + (ยง5.4: `--to` โ†’ non-interactive default โ†’ interactive prompt โ†’ default); + `--page` (picker) and `--page=NAME` (direct); the interactive selectโ†’resolveโ†’confirm + flow (ยง5.5); conflict detection for two pages resolving to the same target (ยง5.6); + optional post-publish rebuild in interactive mode only (ยง5.7); `OverwritePolicy` + + `--force`; `--to` validation (under `_pages/`, `.blade.php`). +- **Out of scope:** views, config, deprecation. Reuse the multiselect helper from + Step 4 โ€” do not fork or rewrite it. +- **Files:** `PagesPublisher`, edits to `PublishCommand`'s `publishPage()` only, and + pages-flow tests. +- **Depends on:** Steps 1, 2, 3 (and the helper from 4). +- **Done when:** tests cover each resolution branch, `--to` validation failures, + conflict detection, and that rebuild only offers in interactive mode. Suite green. +- **Spec ref:** ยง5. + +## Step 6 โ€” `hyde-config` publish tag + +- **Goal:** move config publishing to `vendor:publish` as a single tag. +- **In scope:** register a `hyde-config` publish tag on the relevant service provider + that publishes exactly the six Hyde-owned configs (hyde, docs, markdown, view, cache, + commands) and **not** `torchlight.php`; confirm the `--config` redirect message from + Step 3 names this tag. +- **Out of scope:** touching the `publish` command's views/pages logic; adding config + back into `publish`; anything about Torchlight's own tag. +- **Files:** the service-provider tag registration + a test asserting the tag's file set + (and excluding torchlight). +- **Depends on:** Step 3 (for the redirect message target). +- **Done when:** a test asserts `vendor:publish --tag=hyde-config` maps to exactly the + six files. Suite green. +- **Spec ref:** ยง6. + +## Step 7 โ€” Docs cleanup + +- **Goal:** align the docs with the new command surface. +- **In scope:** fix the nonexistent `php hyde publish:components` reference in + `docs/digging-deeper/advanced-markdown.md` (โ†’ `php hyde publish --components`); + rewrite the publishing docs around `php hyde publish` (views + `--page`) and + `php hyde vendor:publish --tag=hyde-config`; add a short migration note listing the + removed legacy commands and their replacements. +- **Out of scope:** any code changes. Docs only. +- **Files:** the affected docs pages only. +- **Depends on:** Steps 4โ€“6. +- **Done when:** no doc references a nonexistent command; the removed legacy commands + appear only in the migration note, not the primary flow. +- **Spec ref:** ยง12, ยง8. + +## Step 8 (optional) โ€” Acceptance sweep + +- **Goal:** verify nothing drifted across steps. +- **In scope:** walk ยง11 criteria 1โ€“16 one by one; for any not already covered by an + existing test, add a focused test or fix the gap. No new behavior. +- **Out of scope:** new features, refactors, scope beyond ยง11. +- **Files:** test files (+ minimal fixes if a criterion fails). +- **Depends on:** Steps 1โ€“8. +- **Done when:** every ยง11 criterion has a passing test or a demonstrated pass. +- **Spec ref:** ยง11. + +--- + +## Suggested review checklist (you, between steps) + +- Does the diff touch only the files the step named? +- Did it add any flag / command / behavior not in the spec? (If yes โ†’ revert it.) +- Do the new tests actually assert the spec's wording (output strings, error text)? +- Does the full suite still pass? +- One commit, referencing the step number? diff --git a/new-publish-command-spec.md b/new-publish-command-spec.md new file mode 100644 index 00000000000..07224b0d93d --- /dev/null +++ b/new-publish-command-spec.md @@ -0,0 +1,422 @@ +# HydePHP v3 โ€” `publish` Command Specification + +Status: implementation-ready +Scope: reimplement publishing from scratch for v3 +Owner: Emma / HydePHP core + +--- + +## 1. Goals + +Collapse the current four-command publishing surface into a small, Laravel-shaped set: + +- `php hyde publish` โ€” a **flag-driven, views-centric** command for Hyde Blade + customizations, with an optional `--page` side path for starter pages. +- `php hyde vendor:publish` โ€” unchanged Laravel path for providers/tags/packages. + **Config files live here** (see ยง6). + +Design constraints (HydePHP philosophy doc): +- Simplicity first โ€” one entry point for the common case (views). +- Feel Laravel-y โ€” optional flags, no positional sub-arguments. +- Curated over generic โ€” `publish` never exposes tag/provider/path publishing. +- Power still available โ€” advanced users drop to `vendor:publish`. +- Safe by default โ€” never destroy user-modified files silently. + +--- + +## 2. Command surface + +``` +php hyde publish [--layouts] [--components] [--all] [--page[=NAME]] [--to=PATH] [--force] +``` + +All flags are **optional**. With no flags, `publish` runs an interactive wizard. +Each flag simply **skips a wizard step**. + +| Flag | Meaning | +|----------------|--------------------------------------------------------------| +| `--layouts` | Scope to layout views (skip the "what?" step) | +| `--components` | Scope to component views | +| `--all` | Publish **all views**, skip the picker | +| `--page` | Skip to the starter-page picker | +| `--page=NAME` | Publish a specific starter page directly | +| `--to=PATH` | Destination for a published page (**pages only**) | +| `--force` | Overwrite user-modified files | + +**Hard guardrails** + +- `publish` MUST reject `--tag`, `--provider`, and arbitrary source paths: + - `php hyde publish --tag=foo` โ†’ `Use php hyde vendor:publish --tag=foo for tag/provider publishing.` +- `--layouts` and `--components` are mutually exclusive โ†’ fail with guidance. +- `--to` is valid only with `--page` โ†’ else `--to is only valid when publishing a page.` +- `--all` means **all views**; it does not apply to pages. +- Config is not a `publish` concept โ†’ `php hyde publish --config` fails and points + to `vendor:publish --tag=hyde-config`. + +--- + +## 3. Interactive flow (no flags) + +Step 1 โ€” what to publish: + +``` +What do you want to publish? + + โ€บ Views Customize Hyde Blade layouts/components + โ€บ A starter page Copy a homepage, 404, or other default page + โ€บ Cancel +``` + +- Choosing **Views** โ†’ the views multi-select picker (ยง4). +- Choosing **A starter page** โ†’ the page flow (ยง5). + +Flags skip Step 1 (and sometimes Step 2): + +- `--layouts` / `--components` โ†’ jump to the views picker, prefiltered. +- `--all` โ†’ publish all views, no picker. +- `--page` โ†’ jump to the page picker. +- `--page=NAME` โ†’ publish that page, no picker. + +Non-interactive with no flags โ†’ fail helpfully: + +``` +Nothing to publish. Try: + php hyde publish --all + php hyde publish --layouts + php hyde publish --page=welcome +``` + +--- + +## 4. Views + +Publishes Hyde Blade overrides to `resources/views/vendor/hyde/`. +Two declared groups (fixed 1:1 file lists โ€” **no per-item class**): + +- `layouts` โ†’ `resources/views/vendor/hyde/layouts/*` +- `components` โ†’ `resources/views/vendor/hyde/components/*` + +### Behavior + +``` +php hyde publish # wizard โ†’ Views โ†’ multi-select picker +php hyde publish --layouts # picker prefiltered to layouts +php hyde publish --components # picker prefiltered to components +php hyde publish --layouts --all # all layouts, no picker +php hyde publish --all # all views, no picker +php hyde publish --force # overwrite modified +``` + +### Multi-select picker (one grouped picker โ€” no required narrowing step) + +``` +Select Hyde views to publish + + [ ] All views + Layouts + [ ] layouts/app.blade.php + [ ] layouts/page.blade.php + [ ] layouts/post.blade.php + + Components + [ ] components/markdown-heading.blade.php + [ ] components/docs/sidebar.blade.php +``` + +`--layouts` / `--components` only prefilter the list. The user may select one, +many, or all files. + +### Output (cardinality-aware) + +``` +Published 1 view to resources/views/vendor/hyde/components/markdown-heading.blade.php +Published 3 views to resources/views/vendor/hyde/layouts +Published all 42 views to resources/views/vendor/hyde +``` + +A mixed run reports the breakdown โ€” how many views were copied, how many were skipped +because already current, and which were left unchanged because modified (with a `--force` +hint); `Published all N views` prints only when the entire offered set was genuinely copied. + +--- + +## 5. Pages (the `--page` side path) + +Publishes starter/default page templates into `_pages/`. +Pages stay in `publish` (not `vendor:publish`) because a template can have +**multiple valid destinations** and carries **display metadata** โ€” neither of +which `vendor:publish` can express. + +### 5.1 `PublishablePage` value object + registry + +```php +final class PublishablePage +{ + public function __construct( + public string $key, // 'posts' + public string $label, // 'Posts feed' + public string $description, // short help text + public string $source, // stub path within the framework + public ?string $defaultTarget, // '_pages/posts.blade.php', or null when the page has no default (always prompt / require --to) + /** @var array path => human label */ + public array $alternativeTargets = [], + public bool $allowCustomTarget = true, + /** @var class-string */ + public string $pageClass = BladePage::class, // controls custom target validation + ) {} +} + +final class PublishablePages +{ + /** @return array */ + public static function all(): array; + public static function get(string $key): ?PublishablePage; + public static function register(PublishablePage $page): void; // extension point +} +``` + +Only pages get a class. Views are fixed 1:1 file lists (a declared map suffices); +pages need branching destinations + metadata, and the registry lets Hyde Cloud / +plugins register their own publishable pages. + +### 5.2 Initial catalog (illustrative โ€” final set TBD by v3 page work) + +| key | default target | alternatives | custom? | +|-----------|--------------------------|---------------------------------------|---------| +| `welcome` | `_pages/index.blade.php` | โ€” | yes | +| `posts` | `_pages/posts.blade.php` | `_pages/index.blade.php` (as homepage)| yes | +| `blank` | *(none โ€” always prompt)* | โ€” | yes | +| `404` | `_pages/404.blade.php` | โ€” | **no** | + +No dedicated "homepage" concept: setting a homepage = publishing a +homepage-capable page to `_pages/index.blade.php`. + +`blank` is a general-purpose empty starter you drop anywhere, so it declares no +default target: in interactive mode its destination is always prompted for, and +non-interactive use must supply `--to` (see ยง5.4). + +### 5.3 Behavior + +``` +php hyde publish --page # page picker +php hyde publish --page=welcome # publish welcome, resolve destination +php hyde publish --page=posts --to=_pages/index.blade.php +php hyde publish --page=welcome --force +``` + +### 5.4 Destination resolution (per selected page) + +`--to` names a single destination, so it is only valid when publishing a **single named page** +(`--page=NAME --to=PATH`). A bare `--page` (multi-select) combined with `--to` is rejected, since +one path cannot stand in as the destination for several pages. `--to` is also rejected for a page +that declares `allowCustomTarget = false` (e.g. `404`), which has one fixed destination. + +1. `--to=PATH` โ†’ use it. Rejected if the page disallows custom targets; otherwise it must resolve + to a source file for the publishable page's configured `pageClass`. For the default catalog this + means a `BladePage` source path such as `_pages/index.blade.php`, but custom `source_root`, + `source_directories`, file extensions, and extended page classes are respected. +2. Non-interactive, no `--to` โ†’ use `defaultTarget`. If `defaultTarget` is null + (e.g. `blank`), there is nothing to fall back to โ†’ fail helpfully, pointing to `--to`. +3. Interactive AND (`alternativeTargets` non-empty OR `defaultTarget` is null) โ†’ prompt. `allowCustomTarget` + governs only whether a "Custom pathโ€ฆ" entry is offered here (and whether `--to` is accepted), **not** + whether the prompt appears โ€” a page whose default is its one sensible destination (e.g. `welcome`, `404`) + is not prompted for, keeping the common case a single, frictionless step. Its custom placement, when + allowed, is reached through `--to`. + + ``` + Where should "Posts feed" be published? + + โ€บ _pages/posts.blade.php (default โ€” served at /posts) + โ€บ _pages/index.blade.php (use as your site homepage) + โ€บ Custom pathโ€ฆ + ``` +4. Otherwise โ†’ `defaultTarget`. + +### 5.5 Interactive page flow (select โ†’ resolve โ†’ confirm) + +``` +Select pages to publish + + [ ] Welcome page โ†’ _pages/index.blade.php + [ ] Posts feed โ†’ _pages/posts.blade.php + [ ] 404 page โ†’ _pages/404.blade.php +``` + +Resolve ambiguous destinations (ยง5.4), then confirm: + +``` +Ready to publish: + Welcome page โ†’ _pages/index.blade.php + 404 page โ†’ _pages/404.blade.php + +Proceed? [yes] +``` + +### 5.6 Destination conflict detection + +If two selected pages resolve to the same target: + +``` +Welcome page and Blank page both target _pages/index.blade.php. +Pick one, or set --to for each. +``` + +### 5.7 Optional rebuild (interactive only) + +After a successful page publish in interactive mode: + +``` +Rebuild the site now? [no] +``` + +Non-interactive mode NEVER rebuilds automatically. + +--- + +## 6. Config โ€” moved to `vendor:publish` + +Config publishing is rare and has fixed destinations, so it becomes a plain tag: + +``` +php hyde vendor:publish --tag=hyde-config +``` + +`hyde-config` publishes the Hyde-owned config files: +`hyde.php`, `docs.php`, `markdown.php`, `view.php`, `cache.php`, `commands.php`. + +- Torchlight is **not** included โ€” it is obtained via Torchlight's own package tag. +- `hyde-config` is the **only** Hyde config publish tag in v3. The legacy tags + (`configs`, `hyde-configs`, `support-configs`) are removed โ€” they publish nothing. +- Updating existing config files requires `--force`: `vendor:publish` skips files + that already exist without it. +- `publish` has no config concept. `php hyde publish --config` fails and points here. + +> (Exact tag name `hyde-config` is bikesheddable; keep it singular and Hyde-scoped.) + +--- + +## 7. Overwrite policy (unified across views + pages) + +Identical rule everywhere. **No historical-checksum manifest.** + +"Identical" is compared **EOL-agnostically** (line endings normalized before +comparison), reusing Hyde's existing `unixsum` checksum. A file that differs from +the source only by line endings counts as unchanged (`skip`), not modified โ€” CRLF/LF +differences from git autocrlf, editors, or OS are noise, never a user modification. + +| Destination state | Action | +|-----------------------------------------|------------------------------------------| +| Missing | copy | +| Identical to source (EOL-normalized) | skip (`already current`) | +| Exists and differs (user-modified) | require interactive confirm OR `--force` | + +Interactive conflict prompt: + +``` +2 selected files already exist and appear modified. + + โ€บ Skip modified files + โ€บ Overwrite modified files + โ€บ Cancel +``` + +Non-interactive conflict: + +``` +Cannot overwrite modified files without --force: + resources/views/vendor/hyde/layouts/app.blade.php + +Run again with --force to overwrite. +``` + +--- + +## 8. Removed commands (v3) + +v3 is a major release, so the three legacy publish commands are **removed, not +aliased**. There is no compat shim and no deprecation-notice interceptor: invoking +a removed command produces Symfony's native "command not found" error (which already +suggests `publish` as an alternative). The upgrade guide is the migration hint. + +| Removed command | Use instead | +|-------------------------------|-------------------------------------------| +| `publish:views [group]` | `publish --all` / `--layouts` / `--components` | +| `publish:configs` | `vendor:publish --tag=hyde-config --force` | +| `publish:homepage [template]` | `publish --page=[template]` | + +The full replacement table โ€” including the `publish:homepage posts` / `blank` +mappings that do **not** map 1:1 (they need `--to=_pages/index.blade.php`) โ€” lives in +the v3 upgrade guide's "Removed Publishing Commands" section, which is the canonical +migration reference. + +--- + +## 9. Errors & guardrails (summary) + +- `publish --tag=โ€ฆ` / `--provider=โ€ฆ` / raw path โ†’ redirect to `vendor:publish`. +- `publish --config` โ†’ redirect to `vendor:publish --tag=hyde-config`. +- `--layouts` + `--components` together โ†’ mutually exclusive error. +- `--to` without `--page` โ†’ `--to is only valid when publishing a page.` +- `--to` path outside the selected publishable page class's source directory or with the wrong extension โ†’ fail with a valid example. +- Non-interactive with no actionable flags โ†’ fail with usage examples. + +--- + +## 10. Architecture summary โ€” what to build + +1. `PublishCommand` โ€” single flag-driven command; routes to views or page flow. +2. `ViewsPublisher` โ€” reads declared `layouts`/`components` groups. +3. `PagesPublisher` โ€” reads `PublishablePages` registry, resolves destinations, + detects conflicts. +4. `PublishablePage` value object + `PublishablePages` registry (extension point). +5. Shared `OverwritePolicy` service (missing / identical / modified). +6. Shared interactive multi-select + confirmation UI component. + +(No alias/compat commands: the legacy `publish:*` commands are removed outright โ€” see ยง8.) +8. Register the `hyde-config` publish tag on the relevant service provider. + +Views stay declarative file-group lists (no per-item class); only pages use the +value-object + registry model. + +--- + +## 11. Acceptance criteria + +1. `php hyde publish` is flag-driven with no positional sub-arguments. +2. With no flags, `publish` runs the wizard (Views / A starter page). +3. `--layouts`, `--components`, `--all`, `--page`, `--page=NAME`, `--to`, `--force` + each behave per ยง2 and skip the appropriate wizard step. +4. `--layouts` + `--components` together is rejected. +5. `publish` never exposes provider/tag/path publishing; `--tag`/`--provider` + fail and point to `vendor:publish`. +6. `publish` has no config path; `--config` redirects to `vendor:publish --tag=hyde-config`. +7. `vendor:publish --tag=hyde-config` publishes the six Hyde-owned configs and + NOT `torchlight.php`. +8. Views use one grouped multi-select picker; one/many/all all work; output is + cardinality-aware. +9. Pages are backed by the `PublishablePages` registry. +10. A page with multiple valid targets resolves via `--to`, default, or an + interactive destination prompt (custom paths allowed where declared). +11. `--to` is valid only with `--page`; `--all` applies only to views. +12. Two selected pages resolving to the same destination are detected before any write. +13. Overwrite policy is identical for views and pages: missingโ†’copy, + identicalโ†’skip, modifiedโ†’confirm-or-`--force`. No checksum manifest. +14. Modified files are never overwritten without interactive confirm or `--force`. +15. Non-interactive mode never prompts and fails helpfully on ambiguity. +16. The legacy commands (`publish:views`, `publish:configs`, `publish:homepage`) + are removed; invoking them raises the native command-not-found error, and the + upgrade guide documents each replacement (including the `posts`/`blank` `--to` + mappings). + +--- + +## 12. Docs cleanup + +- Remove the reference to `php hyde publish:components` in + `docs/digging-deeper/advanced-markdown.md` (command does not exist). + Replace with `php hyde publish --components`. +- Rewrite the publishing docs around `php hyde publish` (views + `--page`) as the + primary command, and `php hyde vendor:publish --tag=hyde-config` for config. + Document `vendor:publish` as the advanced Laravel path; the removed legacy + commands are covered only by the upgrade guide's "Removed Publishing Commands" + migration table. diff --git a/package-lock.json b/package-lock.json index 4779f2e5cd6..e110d165cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,45 @@ "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.3.2", "autoprefixer": "^10.4.27", - "hyde-vite-plugin": "^1.1.0", + "hyde-vite-plugin": "file:packages/vite-plugin", "hydefront": "^4.0.0", "postcss": "^8.5.9", "tailwindcss": "^4.1.16", - "vite": "^7.3.2" + "vite": "^8.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -28,6 +62,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -45,6 +80,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -62,6 +98,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -79,6 +116,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -96,6 +134,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -113,6 +152,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -130,6 +170,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -147,6 +188,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -164,6 +206,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -181,6 +224,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -198,6 +242,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -215,6 +260,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -232,6 +278,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -249,6 +296,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -266,6 +314,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -283,6 +332,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -300,6 +350,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -317,6 +368,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -334,6 +386,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -351,6 +404,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -368,6 +422,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -385,6 +440,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -402,6 +458,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -419,6 +476,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -436,6 +494,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -453,6 +512,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -489,6 +549,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -507,24 +578,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz", + "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==", "cpu": [ "arm64" ], @@ -533,12 +619,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==", "cpu": [ "arm64" ], @@ -547,12 +636,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz", + "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==", "cpu": [ "x64" ], @@ -561,26 +653,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz", + "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==", "cpu": [ "x64" ], @@ -589,26 +670,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz", + "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==", "cpu": [ "arm" ], @@ -617,26 +687,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==", "cpu": [ "arm64" ], @@ -645,54 +704,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz", + "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==", "cpu": [ "ppc64" ], @@ -701,40 +738,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==", "cpu": [ "s390x" ], @@ -743,12 +755,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==", "cpu": [ "x64" ], @@ -757,12 +772,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz", + "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==", "cpu": [ "x64" ], @@ -771,26 +789,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==", "cpu": [ "arm64" ], @@ -799,40 +806,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz", + "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.9.1", + "@emnapi/runtime": "1.9.1", + "@napi-rs/wasm-runtime": "^1.1.2" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz", + "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz", + "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==", "cpu": [ "x64" ], @@ -841,21 +859,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@tailwindcss/node": { "version": "4.3.2", @@ -990,9 +1004,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1010,9 +1021,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1030,9 +1038,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1050,9 +1055,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1220,12 +1222,39 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, "node_modules/autoprefixer": { "version": "10.4.27", @@ -1310,6 +1339,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/caniuse-lite": { "version": "1.0.30001800", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", @@ -1330,6 +1366,13 @@ } ] }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1381,6 +1424,8 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -1481,14 +1526,8 @@ "license": "ISC" }, "node_modules/hyde-vite-plugin": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hyde-vite-plugin/-/hyde-vite-plugin-1.1.0.tgz", - "integrity": "sha512-T0vii08Y+C1oSDx3NpbkJf4Me6l2bhH6zyD6n00n/W9O9dvLrsMddmFGzrz1I7Cj2Tce7g4Ec2DaFHRJ1tL54A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": ">=6.3.5 <8.0.0" - } + "resolved": "packages/vite-plugin", + "link": true }, "node_modules/hydefront": { "version": "4.0.0", @@ -1874,49 +1913,48 @@ "dev": true, "license": "MIT" }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz", + "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.123.0", + "@rolldown/pluginutils": "1.0.0-rc.13" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.0-rc.13", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.13", + "@rolldown/binding-darwin-x64": "1.0.0-rc.13", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, "node_modules/source-map-js": { @@ -1929,6 +1967,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/tailwindcss": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", @@ -1950,6 +1999,25 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1967,6 +2035,35 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2006,17 +2103,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz", + "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.13", "tinyglobby": "^0.2.15" }, "bin": { @@ -2033,9 +2129,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -2048,13 +2145,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -2079,6 +2179,21 @@ "optional": true } } + }, + "packages/vite-plugin": { + "name": "hyde-vite-plugin", + "version": "1.1.0", + "dev": true, + "license": "MIT", + "devDependencies": { + "@types/node": "^20.0.0", + "terser": "^5.29.2", + "typescript": "^5.0.0", + "vite": "^8.0.0" + }, + "peerDependencies": { + "vite": ">=6.3.5 <9.0.0" + } } } } diff --git a/package.json b/package.json index 3000c76f6d6..ac6dbb0472c 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.3.2", "autoprefixer": "^10.4.27", - "hyde-vite-plugin": "^1.1.0", + "hyde-vite-plugin": "file:packages/vite-plugin", "hydefront": "^4.0.0", "postcss": "^8.5.9", "tailwindcss": "^4.1.16", - "vite": "^7.3.2" + "vite": "^8.0.0" } } diff --git a/packages/framework/resources/views/homepages/welcome.blade.php b/packages/framework/resources/views/homepages/welcome.blade.php index ed3a8c6175a..dfbdf321fad 100644 --- a/packages/framework/resources/views/homepages/welcome.blade.php +++ b/packages/framework/resources/views/homepages/welcome.blade.php @@ -52,7 +52,7 @@ class="relative mt-2 text-transparent bg-clip-text bg-linear-to-br logo-gradient This is the default homepage stored as index.blade.php, however you can publish any of the built-in views using the following command: -
php hyde publish:homepage
+
php hyde publish --page

diff --git a/packages/framework/src/Console/Commands/BuildSiteCommand.php b/packages/framework/src/Console/Commands/BuildSiteCommand.php index c42b1202be7..0f0fadfe443 100644 --- a/packages/framework/src/Console/Commands/BuildSiteCommand.php +++ b/packages/framework/src/Console/Commands/BuildSiteCommand.php @@ -29,9 +29,7 @@ class BuildSiteCommand extends Command protected $signature = 'build {--vite : Build frontend assets using Vite} {--pretty-urls : Should links in output use pretty URLs?} - {--no-api : Disable API calls, for example, Torchlight} - {--run-dev : [Removed] Use --vite instead} - {--run-prod : [Removed] Use --vite instead}'; + {--no-api : Disable API calls, for example, Torchlight}'; /** @var string */ protected $description = 'Build the static site'; @@ -41,8 +39,6 @@ class BuildSiteCommand extends Command public function handle(): int { - $this->checkForDeprecatedRunMixCommandUsage(); - $timeStart = microtime(true); $this->title('Building your static site!'); @@ -157,23 +153,4 @@ protected function getExitCode(): int return Command::SUCCESS; } - - /** - * This method is called when the removed --run-dev or --run-prod options are used. - * - * @deprecated Use --vite instead - * @since v2.0 - This will be removed after 2-3 minor releases depending on the timeframe between them. (~v2.3) - * - * @codeCoverageIgnore - */ - protected function checkForDeprecatedRunMixCommandUsage(): void - { - if ($this->option('run-dev') || $this->option('run-prod')) { - $this->error('The --run-dev and --run-prod options have been removed in HydePHP v2.0.'); - $this->info('Please use --vite instead to build assets for production with Vite.'); - $this->line('See https://github.com/hydephp/develop/pull/2013 for more information.'); - - exit(Command::INVALID); - } - } } diff --git a/packages/framework/src/Console/Commands/PublishCommand.php b/packages/framework/src/Console/Commands/PublishCommand.php new file mode 100644 index 00000000000..10f17200d4f --- /dev/null +++ b/packages/framework/src/Console/Commands/PublishCommand.php @@ -0,0 +1,161 @@ +hasParameterOption(['--tag', '--provider', '--config'], true)) { + $this->output = $output instanceof OutputStyle ? $output : $this->laravel->make( + OutputStyle::class, ['input' => $input, 'output' => $output] + ); + + return $this->redirectRawPublishingFlags($input); + } + + return parent::run($input, $output); + } + + protected function safeHandle(): int + { + if ($this->option('layouts') && $this->option('components')) { + $this->error('The --layouts and --components options are mutually exclusive. Use --all to publish both.'); + + return Command::FAILURE; + } + + if ($this->hasEmptyPageOption()) { + $this->error('The --page option cannot be empty. Use --page for the picker or --page=welcome.'); + + return Command::FAILURE; + } + + if ($this->option('to') !== null && ! $this->wantsToPublishPage()) { + $this->error('--to is only valid when publishing a page.'); + + return Command::FAILURE; + } + + if ($this->wantsToPublishPage()) { + return $this->publishPage(); + } + + if ($this->wantsToPublishViews()) { + return $this->publishViews(); + } + + if (! $this->input->isInteractive()) { + return $this->failWithUsageHint(); + } + + return $this->runWizard(); + } + + protected function runWizard(): int + { + $choice = select('What do you want to publish?', [ + 'views' => 'Views โ€” customize Hyde Blade layouts and components', + 'page' => 'A starter page โ€” copy a homepage, 404, or other default page', + 'cancel' => 'Cancel', + ], 'views'); + + return match ($choice) { + 'views' => $this->publishViews(), + 'page' => $this->publishPage(), + default => Command::SUCCESS, // Cancelling is a clean exit, not an error. + }; + } + + protected function publishViews(): int + { + return (new ViewsPublisher(new PublisherConsole($this, $this->input)))->publish(); + } + + protected function publishPage(): int + { + return (new PagesPublisher(new PublisherConsole($this, $this->input)))->publish(); + } + + protected function wantsToPublishViews(): bool + { + return $this->option('layouts') || $this->option('components') || $this->option('all'); + } + + /** + * The --page flag is value-optional, so a bare --page and an absent --page both read as null + * via option(). We check Symfony's parsed options for its presence to tell the two apart. + */ + protected function wantsToPublishPage(): bool + { + return $this->option('page') !== null || $this->optionWasProvided('page'); + } + + protected function hasEmptyPageOption(): bool + { + return $this->option('page') === ''; + } + + protected function optionWasProvided(string $name): bool + { + return $this->input->hasParameterOption("--$name"); + } + + protected function failWithUsageHint(): int + { + $this->error('Nothing to publish. Try:'); + $this->line(' php hyde publish --all'); + $this->line(' php hyde publish --layouts'); + $this->line(' php hyde publish --page=welcome'); + + return Command::FAILURE; + } + + protected function redirectRawPublishingFlags(InputInterface $input): int + { + if ($input->hasParameterOption('--config', true)) { + $this->error('Config is not published through this command. Use php hyde vendor:publish --tag=hyde-config instead.'); + + return Command::FAILURE; + } + + $flag = $input->hasParameterOption('--tag', true) ? 'tag' : 'provider'; + $value = $input->getParameterOption("--$flag", null, true); + $hint = is_string($value) && $value !== '' ? "--$flag=$value" : "--$flag"; + + $this->error("Use php hyde vendor:publish $hint for tag/provider publishing."); + + return Command::FAILURE; + } +} diff --git a/packages/framework/src/Console/Commands/PublishConfigsCommand.php b/packages/framework/src/Console/Commands/PublishConfigsCommand.php deleted file mode 100644 index 496d88ec3fd..00000000000 --- a/packages/framework/src/Console/Commands/PublishConfigsCommand.php +++ /dev/null @@ -1,52 +0,0 @@ -hyde-configs: Main configuration files', - 'support-configs: Laravel and package configuration files', - ]; - $selection = $this->choice('Which configuration files do you want to publish?', $options, 'All configs'); - - $tag = $this->parseTagFromSelection($selection, $options); - - Artisan::call('vendor:publish', [ - '--tag' => $tag, - '--force' => true, - ], $this->output); - - $this->infoComment(sprintf('Published config files to [%s]', Hyde::path('config'))); - - return Command::SUCCESS; - } - - protected function parseTagFromSelection(string $selection, array $options): string - { - $tags = ['configs', 'hyde-configs', 'support-configs']; - - return $tags[array_search($selection, $options)]; - } -} diff --git a/packages/framework/src/Console/Commands/PublishHomepageCommand.php b/packages/framework/src/Console/Commands/PublishHomepageCommand.php deleted file mode 100644 index 61f49beb9c4..00000000000 --- a/packages/framework/src/Console/Commands/PublishHomepageCommand.php +++ /dev/null @@ -1,128 +0,0 @@ - */ - protected array $options = [ - 'welcome' => [ - 'name' => 'Welcome', - 'description' => 'The default welcome page.', - 'group' => 'hyde-welcome-page', - ], - 'posts' => [ - 'name' => 'Posts Feed', - 'description' => 'A feed of your latest posts. Perfect for a blog site!', - 'group' => 'hyde-posts-page', - ], - 'blank' => [ - 'name' => 'Blank Starter', - 'description' => 'A blank Blade template with just the base layout.', - 'group' => 'hyde-blank-page', - ], - ]; - - public function handle(): int - { - $selected = $this->parseSelection(); - - if (! $this->canExistingFileBeOverwritten()) { - $this->error('A modified index.blade.php file already exists. Use --force to overwrite.'); - - return 409; - } - - $tagExists = array_key_exists($selected, $this->options); - - Artisan::call('vendor:publish', [ - '--tag' => $this->options[$selected]['group'] ?? $selected, - '--force' => true, - ], $tagExists ? null : $this->output); // If the tag doesn't exist, we pass the output to use the called command's error output. - - if ($tagExists) { - $this->infoComment("Published page [$selected]"); - - $this->askToRebuildSite(); - } - - return $tagExists ? Command::SUCCESS : 404; - } - - protected function parseSelection(): string - { - return $this->argument('homepage') ?? $this->parseChoiceIntoKey($this->promptForHomepage()); - } - - protected function promptForHomepage(): string - { - return $this->choice( - 'Which homepage do you want to publish?', - $this->formatPublishableChoices(), - 0 - ); - } - - protected function formatPublishableChoices(): array - { - return $this->getTemplateOptions()->map(function (array $option, string $key): string { - return "$key: {$option['description']}"; - })->values()->toArray(); - } - - /** @return Collection */ - protected function getTemplateOptions(): Collection - { - return new Collection($this->options); - } - - protected function parseChoiceIntoKey(string $choice): string - { - return strstr(str_replace(['', ''], '', $choice), ':', true); - } - - protected function canExistingFileBeOverwritten(): bool - { - if ($this->option('force')) { - return true; - } - - if (! file_exists(BladePage::path('index.blade.php'))) { - return true; - } - - return $this->isTheExistingFileADefaultOne(); - } - - protected function isTheExistingFileADefaultOne(): bool - { - return ViewDiffService::checksumMatchesAny(unixsum_file(BladePage::path('index.blade.php'))); - } -} diff --git a/packages/framework/src/Console/Commands/PublishViewsCommand.php b/packages/framework/src/Console/Commands/PublishViewsCommand.php deleted file mode 100644 index 96e47972f80..00000000000 --- a/packages/framework/src/Console/Commands/PublishViewsCommand.php +++ /dev/null @@ -1,158 +0,0 @@ - */ - protected array $options; - - public function handle(): int - { - $this->options = static::mapToKeys([ - ViewPublishGroup::fromGroup('hyde-layouts', 'Blade Layouts', 'Shared layout views, such as the app layout, navigation menu, and Markdown page templates'), - ViewPublishGroup::fromGroup('hyde-components', 'Blade Components', 'More or less self contained components, extracted for customizability and DRY code'), - ]); - - $selected = ($this->argument('group') ?? $this->promptForGroup()) ?: 'all'; - - if ($selected !== 'all' && (bool) $this->argument('group') === false && ConsoleHelper::canUseLaravelPrompts($this->input)) { - $this->infoComment(sprintf('Selected group [%s]', $selected)); - } - - if (! in_array($selected, $allowed = array_merge(['all'], array_keys($this->options)), true)) { - $this->error("Invalid selection: '$selected'"); - $this->infoComment('Allowed values are: ['.implode(', ', $allowed).']'); - - return Command::FAILURE; - } - - $files = $selected === 'all' - ? collect($this->options)->flatMap(fn (ViewPublishGroup $option): array => $option->publishableFilesMap())->all() - : $this->options[$selected]->publishableFilesMap(); - - $publisher = $this->publishSelectedFiles($files, $selected === 'all'); - - $this->infoComment($publisher->formatOutput($selected)); - - return Command::SUCCESS; - } - - protected function promptForGroup(): string - { - SelectPrompt::fallbackUsing(function (SelectPrompt $prompt): string { - return $this->choice($prompt->label, $prompt->options, $prompt->default); - }); - - return $this->parseChoiceIntoKey( - select('Which group do you want to publish?', $this->formatPublishableChoices(), 0) ?: 'all' - ); - } - - protected function formatPublishableChoices(): array - { - return collect($this->options) - ->map(fn (ViewPublishGroup $option, string $key): string => sprintf('%s: %s', $key, $option->description)) - ->prepend('Publish all groups listed below') - ->values() - ->all(); - } - - protected function parseChoiceIntoKey(string $choice): string - { - return strstr(str_replace(['', ''], '', $choice), ':', true) ?: ''; - } - - /** - * @param array $groups - * @return array - */ - protected static function mapToKeys(array $groups): array - { - return collect($groups)->mapWithKeys(function (ViewPublishGroup $group): array { - return [Str::after($group->group, 'hyde-') => $group]; - })->all(); - } - - /** @param array $files */ - protected function publishSelectedFiles(array $files, bool $isPublishingAll): InteractivePublishCommandHelper - { - $publisher = new InteractivePublishCommandHelper($files); - - if (! $isPublishingAll && ConsoleHelper::canUseLaravelPrompts($this->input)) { - $publisher->only($this->promptUserForWhichFilesToPublish($publisher->getFileChoices())); - } - - $publisher->publishFiles(); - - return $publisher; - } - - /** - * @param array $files - * @return array - */ - protected function promptUserForWhichFilesToPublish(array $files): array - { - $choices = array_merge(['all' => 'All files'], $files); - - $prompt = new MultiSelectPrompt('Select the files you want to publish', $choices, [], 10, 'required', hint: 'Navigate with arrow keys, space to select, enter to confirm.'); - - $prompt->on('key', static::supportTogglingAll($prompt)); - - return (array) $prompt->prompt(); - } - - protected static function supportTogglingAll(MultiSelectPrompt $prompt): Closure - { - return function (string $key) use ($prompt): void { - static $isToggled = false; - - if ($prompt->isHighlighted('all')) { - if ($key === Key::SPACE) { - $prompt->emit('key', Key::CTRL_A); - - if ($isToggled) { - // We need to emit CTRL+A twice to deselect all for some reason - $prompt->emit('key', Key::CTRL_A); - $isToggled = false; - } else { - $isToggled = true; - } - } elseif ($key === Key::ENTER) { - if (! $isToggled) { - $prompt->emit('key', Key::CTRL_A); - } - - $prompt->state = 'submit'; - } - } - }; - } -} diff --git a/packages/framework/src/Console/Commands/RebuildPageCommand.php b/packages/framework/src/Console/Commands/RebuildPageCommand.php deleted file mode 100644 index f6cdb201858..00000000000 --- a/packages/framework/src/Console/Commands/RebuildPageCommand.php +++ /dev/null @@ -1,119 +0,0 @@ -printDeprecationWarning(); - - if ($this->argument('path') === Hyde::getMediaDirectory()) { - return (new TransferMediaAssets())->run($this->output); - - $this->info('All done!'); - - return Command::SUCCESS; - } - - return $this->makeBuildTask($this->output, $this->getNormalizedPathString())->run(); - } - - /** - * @deprecated The `rebuild` command is deprecated and will be removed in HydePHP v3.0. - */ - protected function printDeprecationWarning(): void - { - $this->warn('The `rebuild` command is deprecated and will be removed in HydePHP v3.0.'); - $this->line('If you need to build a single page programmatically, use Hyde\Framework\Actions\StaticPageBuilder::handle() instead.'); - $this->newLine(); - } - - protected function getNormalizedPathString(): string - { - return str_replace('\\', '/', unslash($this->argument('path'))); - } - - protected function makeBuildTask(OutputStyle $output, string $path): BuildTask - { - return new class($output, $path) extends BuildTask - { - public static string $message = 'Rebuilding page'; - - protected string $path; - - public function __construct(OutputStyle $output, string $path) - { - $this->output = $output; - $this->path = $path; - } - - public function handle(): void - { - $this->validate(); - - StaticPageBuilder::handle(Pages::getPage($this->path)); - } - - public function printFinishMessage(): void - { - $this->createdSiteFile(Command::fileLink( - Hyde::sitePath(Pages::getPage($this->path)->getOutputPath()) - ))->withExecutionTime(); - } - - protected function validate(): void - { - $directory = Hyde::pathToRelative(dirname($this->path)); - - $directories = [ - Hyde::pathToRelative(BladePage::path()), - Hyde::pathToRelative(BladePage::path()), - Hyde::pathToRelative(MarkdownPage::path()), - Hyde::pathToRelative(MarkdownPost::path()), - Hyde::pathToRelative(DocumentationPage::path()), - ]; - - if (! in_array($directory, $directories)) { - throw new Exception("Path [$this->path] is not in a valid source directory.", 400); - } - - if (! file_exists(Hyde::path($this->path))) { - throw new Exception("File [$this->path] not found.", 404); - } - } - }; - } -} diff --git a/packages/framework/src/Console/Concerns/AsksToRebuildSite.php b/packages/framework/src/Console/Concerns/AsksToRebuildSite.php deleted file mode 100644 index 02ef6c9c0b0..00000000000 --- a/packages/framework/src/Console/Concerns/AsksToRebuildSite.php +++ /dev/null @@ -1,25 +0,0 @@ -option('no-interaction')) { - return; - } - - if ($this->confirm('Would you like to rebuild the site?', true)) { - $this->line('Okay, building site!'); - Artisan::call('build'); - $this->info('Site is built!'); - } else { - $this->line('Okay, you can always run the build later!'); - } - } -} diff --git a/packages/framework/src/Console/ConsoleServiceProvider.php b/packages/framework/src/Console/ConsoleServiceProvider.php index a62819aa05e..15174a9a998 100644 --- a/packages/framework/src/Console/ConsoleServiceProvider.php +++ b/packages/framework/src/Console/ConsoleServiceProvider.php @@ -21,15 +21,12 @@ public function register(): void Commands\BuildSearchCommand::class, Commands\BuildSiteCommand::class, Commands\BuildSitemapCommand::class, - Commands\RebuildPageCommand::class, Commands\MakePageCommand::class, Commands\MakePostCommand::class, Commands\VendorPublishCommand::class, - Commands\PublishConfigsCommand::class, - Commands\PublishHomepageCommand::class, - Commands\PublishViewsCommand::class, + Commands\PublishCommand::class, Commands\PackageDiscoverCommand::class, Commands\RouteListCommand::class, diff --git a/packages/framework/src/Console/Helpers/BasePublisher.php b/packages/framework/src/Console/Helpers/BasePublisher.php new file mode 100644 index 00000000000..1479654f4da --- /dev/null +++ b/packages/framework/src/Console/Helpers/BasePublisher.php @@ -0,0 +1,55 @@ +policyErrors++; + + if (! Filesystem::exists($source)) { + $console->error("Skipped [$target]: source file [$source] does not exist."); + + return; + } + + if (! Filesystem::isFile($source)) { + $console->error("Skipped [$target]: source [$source] is not a file."); + + return; + } + + if (Filesystem::isDirectory($target)) { + $console->error("Skipped [$target]: destination is a directory."); + + return; + } + + $console->error("Skipped [$target]: source or destination is invalid."); + } + + protected function hasPolicyErrors(): bool + { + return $this->policyErrors > 0; + } + + protected function copy(string $source, string $target): void + { + Filesystem::ensureParentDirectoryExists($target); + + if (! Filesystem::copy($source, $target)) { + throw new RuntimeException("Failed to copy [$source] to [$target]."); + } + } +} diff --git a/packages/framework/src/Console/Helpers/InteractiveMultiselect.php b/packages/framework/src/Console/Helpers/InteractiveMultiselect.php new file mode 100644 index 00000000000..831c45031c8 --- /dev/null +++ b/packages/framework/src/Console/Helpers/InteractiveMultiselect.php @@ -0,0 +1,45 @@ + $options Map of option key => display label. + * @param string|null $allLabel Label for the "select all" row, or null to omit it entirely. + * @return array The selected option keys (never includes the sentinel). + */ + public static function select(string $label, array $options, ?string $allLabel = null): array + { + $choices = $allLabel !== null ? [self::ALL => $allLabel] + $options : $options; + + $prompt = new MultiSelectPrompt($label, $choices, [], 10, false, hint: 'Navigate with arrow keys, space to select, enter to confirm.'); + + $selected = $prompt->prompt(); + + // Selecting the sentinel means "everything", regardless of which other rows were checked. + if (in_array(self::ALL, $selected, true)) { + return array_keys($options); + } + + return array_values(array_filter($selected, fn ($key): bool => (string) $key !== self::ALL)); + } +} diff --git a/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php b/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php deleted file mode 100644 index 97f0871292c..00000000000 --- a/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php +++ /dev/null @@ -1,87 +0,0 @@ - Map of source files to target files */ - protected array $publishableFilesMap; - - protected readonly int $originalFileCount; - - /** @param array $publishableFilesMap */ - public function __construct(array $publishableFilesMap) - { - $this->publishableFilesMap = $publishableFilesMap; - $this->originalFileCount = count($publishableFilesMap); - } - - /** @return array */ - public function getFileChoices(): array - { - return Arr::mapWithKeys($this->publishableFilesMap, /** @return array */ function (string $target, string $source): array { - return [$source => $this->pathRelativeToDirectory($source, $this->getBaseDirectory())]; - }); - } - - /** - * Only publish the selected files. - * - * @param array $selectedFiles Array of selected file paths, matching the keys of the publishableFilesMap. - */ - public function only(array $selectedFiles): void - { - $this->publishableFilesMap = Arr::only($this->publishableFilesMap, $selectedFiles); - } - - /** Find the most specific common parent directory path for the files, trimming as much as possible whilst keeping specificity and uniqueness. */ - public function getBaseDirectory(): string - { - $partsMap = collect($this->publishableFilesMap)->map(function (string $file): array { - return explode('/', $file); - }); - - $commonParts = $partsMap->reduce(function (array $carry, array $parts): array { - return array_intersect($carry, $parts); - }, $partsMap->first()); - - return implode('/', $commonParts); - } - - public function publishFiles(): void - { - foreach ($this->publishableFilesMap as $source => $target) { - Filesystem::ensureParentDirectoryExists($target); - Filesystem::copy($source, $target); - } - } - - public function formatOutput(string $group): string - { - $fileCount = count($this->publishableFilesMap); - $publishedOneFile = $fileCount === 1; - $publishedAllGroups = $group === 'all'; - $publishedAllFiles = $fileCount === $this->originalFileCount; - $selectedFilesModifier = $publishedAllFiles ? 'all' : 'selected'; - - return match (true) { - $publishedAllGroups => sprintf('Published all %d files to [%s]', $fileCount, $this->getBaseDirectory()), - $publishedOneFile => sprintf('Published selected file to [%s]', reset($this->publishableFilesMap)), - default => sprintf('Published %s [%s] files to [%s]', $selectedFilesModifier, Str::singular($group), $this->getBaseDirectory()) - }; - } - - protected function pathRelativeToDirectory(string $source, string $directory): string - { - return Str::after($source, basename($directory).'/'); - } -} diff --git a/packages/framework/src/Console/Helpers/PagesPublisher.php b/packages/framework/src/Console/Helpers/PagesPublisher.php new file mode 100644 index 00000000000..de187682380 --- /dev/null +++ b/packages/framework/src/Console/Helpers/PagesPublisher.php @@ -0,0 +1,503 @@ + Pages skipped because the destination is already up to date. */ + protected array $current = []; + + /** @var array Modified destinations left unchanged (interactive skip). */ + protected array $leftModified = []; + + /** Whether the pages were chosen through the interactive picker (which adds a confirmation step). */ + protected bool $usedPicker = false; + + public function __construct(protected PublisherConsole $console) + { + } + + public function publish(): int + { + // --to names a single destination, so it is only meaningful for a single named page. A bare --page + // (multi-select) with --to would have one path stand in for several pages; reject it rather than guess. + if ($this->console->option('to') !== null && ! $this->hasNamedPage()) { + $this->console->error('--to is only valid when publishing a single page. Use --page=NAME with --to.'); + + return Command::FAILURE; + } + + $pages = $this->selectPages(); + + if ($pages === null) { + return Command::FAILURE; // A guidance message was already printed. + } + + if ($pages === []) { + $this->console->infoComment('No pages selected; nothing to publish.'); + + return Command::SUCCESS; + } + + $resolved = $this->resolveDestinations($pages); + + if ($resolved === null) { + // A destination could not be resolved (invalid --to, an invalid custom path, or a page with no + // default in non-interactive mode). A guidance message was already printed; this is always a failure. + return Command::FAILURE; + } + + if (! $this->assertNoDestinationConflicts($resolved)) { + return Command::FAILURE; + } + + if ($this->usedPicker && ! $this->confirmProceed($resolved)) { + $this->console->infoComment('Cancelled. No pages were published.'); + + return Command::SUCCESS; + } + + $written = $this->write($resolved); + + // A null result means the run stopped after the decision but before any write: a non-interactive blocked + // run without --force is a hard failure, while an interactive Cancel is a clean exit. + if ($written === null) { + return $this->console->canPrompt() ? Command::SUCCESS : Command::FAILURE; + } + + $this->report($written); + + if ($written !== []) { + $this->maybeRebuild(); + } + + return $this->hasPolicyErrors() ? Command::FAILURE : Command::SUCCESS; + } + + /** + * Determine which pages to publish: a named page directly, or the interactive picker. + * + * @return array|null The selected pages, or null when the run should fail (message printed). + */ + protected function selectPages(): ?array + { + if ($this->hasNamedPage()) { + $name = (string) $this->console->option('page'); + $page = $this->findPage($name); + + if ($page === null) { + $this->console->error("The page [$name] does not exist."); + $this->console->line('Available pages: '.implode(', ', array_map(fn (PublishablePage $page): string => $page->key, PublishablePages::all()))); + + return null; + } + + return [$page]; + } + + if (! $this->console->canPrompt()) { + $this->console->error('No page specified for publishing. Provide one, for example --page=welcome.'); + + return null; + } + + $this->usedPicker = true; + + return $this->promptForPages(); + } + + /** @return array */ + protected function promptForPages(): array + { + $options = []; + + foreach (PublishablePages::all() as $page) { + $options[$page->key] = $this->pickerLabel($page); + } + + $selected = InteractiveMultiselect::select('Select pages to publish', $options); + + // Cast defends against PHP coercing a numeric key such as '404' to an int on the way back through the prompt. + return array_map(fn ($key): PublishablePage => $this->findPage((string) $key), $selected); + } + + protected function pickerLabel(PublishablePage $page): string + { + return $page->defaultTarget !== null + ? sprintf('%s โ†’ %s', $page->label, $page->defaultTarget) + : $page->label; + } + + /** + * Resolve the destination for each selected page, in registry order. + * + * @param array $pages + * @return array|null Null when resolution failed or was cancelled. + */ + protected function resolveDestinations(array $pages): ?array + { + $resolved = []; + + foreach ($pages as $page) { + $target = $this->resolveTarget($page); + + if ($target === null) { + return null; + } + + $resolved[] = ['page' => $page, 'target' => $this->normalizeTargetPath($target)]; + } + + return $resolved; + } + + protected function resolveTarget(PublishablePage $page): ?string + { + if ($this->console->option('to') !== null) { + if (! $page->allowCustomTarget) { + $this->console->error(sprintf('The [%s] page cannot be published to a custom path; omit --to to use its default (%s).', $page->key, $page->defaultTarget)); + + return null; + } + + return $this->validateCustomTarget((string) $this->console->option('to'), $page); + } + + if (! $this->console->canPrompt()) { + if ($page->defaultTarget === null) { + $this->console->error(sprintf('The [%s] page has no default destination. Provide one with --to.', $page->key)); + + return null; + } + + return $page->defaultTarget; + } + + // Only prompt when the destination is genuinely ambiguous: alternative targets exist, or there is no + // default at all. A page with one sensible default (welcome, 404) is never prompted for. + if ($page->alternativeTargets !== [] || $page->defaultTarget === null) { + return $this->promptForTarget($page); + } + + return $page->defaultTarget; + } + + protected function promptForTarget(PublishablePage $page): ?string + { + $options = []; + + if ($page->defaultTarget !== null) { + $options[$page->defaultTarget] = sprintf('%s (default)', $page->defaultTarget); + } + + foreach ($page->alternativeTargets as $path => $label) { + $options[$path] = sprintf('%s (%s)', $path, $label); + } + + if ($page->allowCustomTarget) { + $options[self::CUSTOM] = 'Custom pathโ€ฆ'; + } + + $choice = (string) select(sprintf('Where should "%s" be published?', $page->label), $options); + + return $choice === self::CUSTOM ? $this->promptForCustomTarget($page) : $choice; + } + + protected function promptForCustomTarget(PublishablePage $page): ?string + { + $path = text( + label: sprintf('Enter a %s source path', class_basename($page->pageClass)), + placeholder: $this->exampleTargetPath($page), + required: true, + validate: fn (string $value): ?string => $this->isValidCustomTarget($value, $page) + ? null + : $this->customTargetPromptError($page) + ); + + return $this->normalizeTargetPath($path); + } + + /** Validate a user-supplied destination against the publishable page's configured Hyde page model. */ + protected function validateCustomTarget(string $path, PublishablePage $page): ?string + { + if (! $this->isValidCustomTarget($path, $page)) { + $this->console->error($this->customTargetOptionError($page)); + + return null; + } + + return $this->normalizeTargetPath($path); + } + + protected function isValidCustomTarget(string $path, PublishablePage $page): bool + { + try { + $normalized = $this->normalizeTargetPath($path); + } catch (FilesystemException) { + return false; + } + + $pageClass = $page->pageClass; + + return $this->targetIsWithinSourceDirectory($normalized, $page->pageClass) + && Str::endsWith($normalized, $pageClass::fileExtension()); + } + + protected function normalizeTargetPath(string $path): string + { + return (new WhitespacePathNormalizer())->normalizePath($path); + } + + /** @param class-string $pageClass */ + protected function targetIsWithinSourceDirectory(string $target, string $pageClass): bool + { + $sourceDirectory = $this->normalizeTargetPath($pageClass::sourceDirectory()); + + return $sourceDirectory === '' + || Str::startsWith($target, "$sourceDirectory/"); + } + + protected function customTargetOptionError(PublishablePage $page): string + { + $pageClass = $page->pageClass; + + return sprintf( + 'The --to path must be within %s and end in %s, for example %s.', + $this->sourceDirectoryLabel($page), + $pageClass::fileExtension(), + $this->customTargetExamplePath($page) + ); + } + + protected function customTargetPromptError(PublishablePage $page): string + { + $pageClass = $page->pageClass; + + return sprintf( + 'The path must be within %s and end in %s.', + $this->sourceDirectoryLabel($page), + $pageClass::fileExtension() + ); + } + + protected function sourceDirectoryLabel(PublishablePage $page): string + { + $pageClass = $page->pageClass; + $sourceDirectory = $this->normalizeTargetPath($pageClass::sourceDirectory()); + + return $sourceDirectory === '' ? 'the project root' : "$sourceDirectory/"; + } + + protected function exampleTargetPath(PublishablePage $page): string + { + $pageClass = $page->pageClass; + + return $this->normalizeTargetPath($pageClass::sourcePath('example')); + } + + protected function customTargetExamplePath(PublishablePage $page): string + { + return $page->defaultTarget !== null + ? $this->normalizeTargetPath($page->defaultTarget) + : $this->exampleTargetPath($page); + } + + /** @param array $resolved */ + protected function assertNoDestinationConflicts(array $resolved): bool + { + $labelsByTarget = []; + + foreach ($resolved as $entry) { + $labelsByTarget[$entry['target']][] = $entry['page']->label; + } + + foreach ($labelsByTarget as $target => $labels) { + if (count($labels) > 1) { + // "both" reads correctly for a pair; three or more colliding pages need "all". + $verb = count($labels) === 2 ? 'both target' : 'all target'; + $this->console->error(sprintf('%s %s %s.', $this->joinLabels($labels), $verb, $target)); + $this->console->line('Pick one, or set --to for each.'); + + return false; + } + } + + return true; + } + + /** @param array $resolved */ + protected function confirmProceed(array $resolved): bool + { + $this->console->line('Ready to publish:'); + + foreach ($resolved as $entry) { + $this->console->line(sprintf(' %s โ†’ %s', $entry['page']->label, $entry['target'])); + } + + $this->console->newLine(); + + return confirm('Proceed?', true); + } + + /** + * Apply the shared overwrite policy and copy the resolved pages into place. + * + * @param array $resolved + * @return array|null The pages actually written, or null when the run should stop (cancelled, or blocked without --force). + */ + protected function write(array $resolved): ?array + { + $copy = []; + $blocked = []; + + foreach ($resolved as $entry) { + $record = [ + 'page' => $entry['page'], + 'target' => $entry['target'], + 'source' => Hyde::vendorPath($entry['page']->source), + 'absolute' => Hyde::path($entry['target']), + ]; + + $action = OverwritePolicy::decide($record['source'], $record['absolute']); + + if ($action === OverwriteAction::Copy) { + $copy[] = $record; + } elseif ($action === OverwriteAction::Skip) { + $this->noteCurrent($record); + } elseif ($action === OverwriteAction::Error) { + $this->reportPolicyError($this->console, $record['source'], $record['absolute']); + } else { + $blocked[] = $record; + } + } + + $overwrite = $this->console->resolveBlocked($blocked, 'Cancelled. No pages were published.'); + + if ($overwrite === null) { + return null; + } + + $this->leftModified = $overwrite === [] ? array_map(fn (array $record): array => ['page' => $record['page'], 'target' => $record['target']], $blocked) : []; + + $written = array_merge($copy, $overwrite); + + foreach ($written as $record) { + $this->copy($record['source'], $record['absolute']); + } + + return $written; + } + + /** + * @param array{page: PublishablePage, target: string, source: string, absolute: string} $record + */ + protected function noteCurrent(array $record): void + { + $this->current[] = ['page' => $record['page'], 'target' => $record['target']]; + } + + /** @param array $written */ + protected function report(array $written): void + { + if ($written === [] && $this->leftModified === [] && $this->current !== []) { + $this->console->infoComment('All selected pages are already up to date.'); + + return; + } + + foreach ($written as $record) { + $this->console->infoComment(sprintf('Published [%s] to [%s]', $record['page']->key, $record['target'])); + } + + if ($this->current !== []) { + $this->console->infoComment(sprintf('%s already up to date and skipped.', $this->pageCount(count($this->current)))); + } + + if ($this->leftModified !== []) { + $this->console->newLine(); + $this->console->warn(sprintf('%s left unchanged because they were modified:', $this->pageCount(count($this->leftModified)))); + + foreach ($this->leftModified as $entry) { + $this->console->line(' '.$entry['target']); + } + + $this->console->line('Run again with --force to overwrite.'); + } + } + + /** + * Interactive only, and deliberately defaulting to NO: a single page publish should not auto-rebuild + * the entire site. Keep this check here rather than moving it into a shared rebuild helper, which + * would tempt a yes-default back in. + */ + protected function maybeRebuild(): void + { + if (! $this->console->canPrompt()) { + return; + } + + if (confirm('Rebuild the site now?', false)) { + Artisan::call('build', [], $this->console->getOutput()); + } + } + + /** Whether a specific page name was supplied via --page=NAME (as opposed to a bare --page or the wizard). */ + protected function hasNamedPage(): bool + { + $name = $this->console->option('page'); + + return is_string($name) && $name !== ''; + } + + /** Find a page by key, comparing the ->key property so a numeric key such as '404' is never lost to array coercion. */ + protected function findPage(string $name): ?PublishablePage + { + foreach (PublishablePages::all() as $page) { + if ($page->key === $name) { + return $page; + } + } + + return null; + } + + /** @param array $labels */ + protected function joinLabels(array $labels): string + { + return Arr::join($labels, ', ', ' and '); + } + + protected function pageCount(int $count): string + { + return "$count ".Str::plural('page', $count); + } +} diff --git a/packages/framework/src/Console/Helpers/PublishablePage.php b/packages/framework/src/Console/Helpers/PublishablePage.php new file mode 100644 index 00000000000..5341a8bee97 --- /dev/null +++ b/packages/framework/src/Console/Helpers/PublishablePage.php @@ -0,0 +1,33 @@ + $alternativeTargets Additional valid destinations, mapping a project-relative path to a human label. + * @param bool $allowCustomTarget Whether the user may publish this page to a custom path. + * @param class-string $pageClass The Hyde page model this page is published as; controls custom target validation. + */ + public function __construct( + public readonly string $key, + public readonly string $label, + public readonly string $description, + public readonly string $source, + public readonly ?string $defaultTarget, + public readonly array $alternativeTargets = [], + public readonly bool $allowCustomTarget = true, + public readonly string $pageClass = BladePage::class, + ) { + } +} diff --git a/packages/framework/src/Console/Helpers/PublishablePages.php b/packages/framework/src/Console/Helpers/PublishablePages.php new file mode 100644 index 00000000000..79555fefdb8 --- /dev/null +++ b/packages/framework/src/Console/Helpers/PublishablePages.php @@ -0,0 +1,87 @@ +|null */ + protected static ?array $pages = null; + + /** @return array */ + public static function all(): array + { + return static::$pages ??= static::getDefaultPages(); + } + + public static function get(string $key): ?PublishablePage + { + return static::all()[$key] ?? null; + } + + /** Overrides any existing page sharing its key. */ + public static function register(PublishablePage $page): void + { + static::$pages = static::all(); + static::$pages[$page->key] = $page; + } + + /** + * Reset the registry back to its default catalog. + * + * @internal Primarily used to restore state between tests. + */ + public static function clear(): void + { + static::$pages = null; + } + + /** @return array */ + protected static function getDefaultPages(): array + { + return static::keyed([ + new PublishablePage( + key: 'welcome', + label: 'Welcome page', + description: 'The default Hyde welcome page.', + source: 'resources/views/homepages/welcome.blade.php', + defaultTarget: BladePage::sourcePath('index'), + ), + new PublishablePage( + key: 'posts', + label: 'Posts feed', + description: 'A feed of your latest posts. Perfect for a blog site!', + source: 'resources/views/homepages/post-feed.blade.php', + defaultTarget: BladePage::sourcePath('posts'), + alternativeTargets: [BladePage::sourcePath('index') => 'Use as your site homepage'], + ), + new PublishablePage( + key: 'blank', + label: 'Blank page', + description: 'A blank Blade template with just the base layout.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: null, + ), + new PublishablePage( + key: '404', + label: '404 page', + description: 'A custom 404 error page.', + source: 'resources/views/pages/404.blade.php', + defaultTarget: BladePage::sourcePath('404'), + allowCustomTarget: false, + ), + ]); + } + + /** + * @param array $pages + * @return array + */ + protected static function keyed(array $pages): array + { + return collect($pages)->keyBy(fn (PublishablePage $page): string => $page->key)->all(); + } +} diff --git a/packages/framework/src/Console/Helpers/PublisherConsole.php b/packages/framework/src/Console/Helpers/PublisherConsole.php new file mode 100644 index 00000000000..a01d94fc4a6 --- /dev/null +++ b/packages/framework/src/Console/Helpers/PublisherConsole.php @@ -0,0 +1,110 @@ +command->option($key); + } + + public function canPrompt(): bool + { + return ConsoleHelper::canUseLaravelPrompts($this->input); + } + + public function error(string $message): void + { + $this->command->error($message); + } + + public function warn(string $message): void + { + $this->command->warn($message); + } + + public function line(string $message): void + { + $this->command->line($message); + } + + public function newLine(): void + { + $this->command->newLine(); + } + + public function infoComment(string $message): void + { + $this->command->infoComment($message); + } + + public function getOutput(): OutputStyle + { + return $this->command->getOutput(); + } + + /** + * @param array $blocked + * @return array|null + */ + public function resolveBlocked(array $blocked, string $cancelledMessage): ?array + { + if ($blocked === []) { + return []; + } + + if ($this->option('force')) { + return $blocked; + } + + if (! $this->canPrompt()) { + $this->error('Cannot overwrite modified files without --force:'); + + foreach ($blocked as $record) { + $this->line(' '.$record['target']); + } + + $this->newLine(); + $this->line('Run again with --force to overwrite.'); + + return null; + } + + $choice = select(sprintf('%d selected files already exist and appear modified.', count($blocked)), [ + 'skip' => 'Skip modified files', + 'overwrite' => 'Overwrite modified files', + 'cancel' => 'Cancel', + ], 'skip'); + + return match ($choice) { + 'overwrite' => $blocked, + 'skip' => [], + default => $this->cancel($cancelledMessage), + }; + } + + protected function cancel(string $message): ?array + { + $this->infoComment($message); + + return null; + } +} diff --git a/packages/framework/src/Console/Helpers/ViewsPublisher.php b/packages/framework/src/Console/Helpers/ViewsPublisher.php new file mode 100644 index 00000000000..22790a262da --- /dev/null +++ b/packages/framework/src/Console/Helpers/ViewsPublisher.php @@ -0,0 +1,249 @@ + Files skipped because already up to date. */ + protected array $current = []; + + public function __construct(protected PublisherConsole $console) + { + } + + /** + * Every file's outcome is decided, and conflicts resolved, before anything is written โ€” so cancelling + * never leaves a half-published tree. + */ + public function publish(): int + { + [$offered, $labels] = $this->collectOfferedFiles(); + + $selected = Arr::only($offered, $this->selectFiles($offered, $labels)); + + if ($selected === []) { + $this->console->infoComment('No views selected; nothing to publish.'); + + return Command::SUCCESS; + } + + [$copy, $current, $blocked] = $this->decide($selected); + $this->current = $current; + + $overwrite = $this->console->resolveBlocked($blocked, 'Cancelled. No views were published.'); + + // A null resolution means the run stopped after the decision but before any write: a non-interactive + // blocked run without --force is a hard failure, while an interactive Cancel is a clean exit. + if ($overwrite === null) { + return $this->console->canPrompt() ? Command::SUCCESS : Command::FAILURE; + } + + $written = array_merge($copy, $overwrite); + + $published = $this->recordsToMap($written); + + foreach ($published as $source => $target) { + $this->copy($source, $target); + } + + $status = $this->report($published, $this->current, $overwrite === [] ? $this->recordsToMap($blocked) : [], count($offered)); + + return $this->hasPolicyErrors() ? Command::FAILURE : $status; + } + + /** + * @return array{0: array, 1: array} A tuple of [source => target] and [source => group-prefixed label] for the offered files. + */ + protected function collectOfferedFiles(): array + { + $offered = []; + $labels = []; + + foreach ($this->groups() as $key => $group) { + foreach ($group->publishableFilesMap() as $source => $target) { + $offered[$source] = $target; + $labels[$source] = $key.'/'.Str::after($source, $group->source.'/'); + } + } + + return [$offered, $labels]; + } + + /** @return array The offered groups, keyed by short name and filtered by --layouts/--components. */ + protected function groups(): array + { + $groups = [ + 'layouts' => ViewPublishGroup::fromGroup('hyde-layouts'), + 'components' => ViewPublishGroup::fromGroup('hyde-components'), + ]; + + if ($this->console->option('layouts')) { + return Arr::only($groups, ['layouts']); + } + + if ($this->console->option('components')) { + return Arr::only($groups, ['components']); + } + + return $groups; + } + + /** + * @param array $offered + * @param array $labels + * @return array The selected source keys. + */ + protected function selectFiles(array $offered, array $labels): array + { + // --all skips the picker; so does a non-interactive run, where publishing a scoped group is + // exactly equivalent to adding --all: one predictable rule, with OverwritePolicy still protecting + // any modified files. + if ($this->console->option('all') || ! $this->console->canPrompt()) { + return array_keys($offered); + } + + return InteractiveMultiselect::select('Select Hyde views to publish', $labels, 'All views'); + } + + /** + * @param array $selected + * @return array{0: array, 1: array, 2: array} A tuple of [copy records, already-current map, blocked records]. + */ + protected function decide(array $selected): array + { + $copy = []; + $current = []; + $blocked = []; + + foreach ($selected as $source => $target) { + $action = OverwritePolicy::decide($source, $target); + $record = $this->record($source, $target); + + if ($action === OverwriteAction::Copy) { + $copy[] = $record; + } elseif ($action === OverwriteAction::Skip) { + $current[$source] = $target; + } elseif ($action === OverwriteAction::Error) { + $this->reportPolicyError($this->console, $source, $target); + } else { + $blocked[] = $record; + } + } + + return [$copy, $current, $blocked]; + } + + /** @return array{source: string, target: string, absolute: string} */ + protected function record(string $source, string $target): array + { + return ['source' => $source, 'target' => $target, 'absolute' => $target]; + } + + /** @param array $records */ + protected function recordsToMap(array $records): array + { + $map = []; + + foreach ($records as $record) { + $map[$record['source']] = $record['target']; + } + + return $map; + } + + /** + * @param array $published The files actually written (source => target). + * @param array $current The files skipped because already up to date. + * @param array $blocked The modified files left unchanged (interactive skip). + */ + protected function report(array $published, array $current, array $blocked, int $offeredTotal): int + { + if ($published === [] && $blocked === [] && $current !== []) { + $this->console->infoComment('All selected views are already up to date.'); + + return Command::SUCCESS; + } + + if ($published !== []) { + $this->console->infoComment($this->publishedLine($published, $offeredTotal)); + } + + if ($current !== []) { + $this->console->infoComment(sprintf('%s already up to date and skipped.', $this->viewCount(count($current)))); + } + + if ($blocked !== []) { + $this->console->newLine(); + $this->console->warn(sprintf('%s left unchanged because they were modified:', $this->viewCount(count($blocked)))); + + foreach ($blocked as $target) { + $this->console->line(' '.$target); + } + + $this->console->line('Run again with --force to overwrite.'); + } + + return Command::SUCCESS; + } + + /** @param array $published */ + protected function publishedLine(array $published, int $offeredTotal): string + { + $count = count($published); + + if ($count === 1) { + return sprintf('Published 1 view to [%s]', reset($published)); + } + + // "all N" is reserved for when the entire offered set was genuinely copied; any file that was + // already current or a blocked modification drops the count below the offered total. + $base = $this->baseDirectory($published); + + return $count === $offeredTotal + ? sprintf('Published all %d views to [%s]', $count, $base) + : sprintf('Published %d views to [%s]', $count, $base); + } + + protected function viewCount(int $count): string + { + return "$count ".Str::plural('view', $count); + } + + /** Find the most specific common parent directory shared by the given files' target paths. */ + protected function baseDirectory(array $files): string + { + $partsMap = collect($files)->map(fn (string $file): array => explode('/', $file)); + $commonParts = []; + + foreach ($partsMap->first() as $index => $part) { + foreach ($partsMap as $parts) { + if (! isset($parts[$index]) || $parts[$index] !== $part) { + break 2; + } + } + + $commonParts[] = $part; + } + + return implode('/', $commonParts); + } +} diff --git a/packages/framework/src/Enums/OverwriteAction.php b/packages/framework/src/Enums/OverwriteAction.php new file mode 100644 index 00000000000..364c1fb058f --- /dev/null +++ b/packages/framework/src/Enums/OverwriteAction.php @@ -0,0 +1,26 @@ +publishes([ - __DIR__.'/../../../config' => config_path(), - ], 'configs'); - $this->publishes([ __DIR__.'/../../../config/hyde.php' => config_path('hyde.php'), __DIR__.'/../../../config/docs.php' => config_path('docs.php'), __DIR__.'/../../../config/markdown.php' => config_path('markdown.php'), - ], 'hyde-configs'); - - $this->publishes([ __DIR__.'/../../../config/view.php' => config_path('view.php'), __DIR__.'/../../../config/cache.php' => config_path('cache.php'), __DIR__.'/../../../config/commands.php' => config_path('commands.php'), - __DIR__.'/../../../config/torchlight.php' => config_path('torchlight.php'), - ], 'support-configs'); + ], 'hyde-config'); } } diff --git a/packages/framework/src/Foundation/Providers/ViewServiceProvider.php b/packages/framework/src/Foundation/Providers/ViewServiceProvider.php index 19cccadef69..8b24df23850 100644 --- a/packages/framework/src/Foundation/Providers/ViewServiceProvider.php +++ b/packages/framework/src/Foundation/Providers/ViewServiceProvider.php @@ -38,18 +38,6 @@ public function boot(): void Hyde::vendorPath('resources/views/pages/404.blade.php') => Hyde::path('_pages/404.blade.php'), ], 'hyde-page-404'); - $this->publishes([ - Hyde::vendorPath('resources/views/homepages/welcome.blade.php') => Hyde::path('_pages/index.blade.php'), - ], 'hyde-welcome-page'); - - $this->publishes([ - Hyde::vendorPath('resources/views/homepages/post-feed.blade.php') => Hyde::path('_pages/index.blade.php'), - ], 'hyde-posts-page'); - - $this->publishes([ - Hyde::vendorPath('resources/views/homepages/blank.blade.php') => Hyde::path('_pages/index.blade.php'), - ], 'hyde-blank-page'); - Blade::component('link', LinkComponent::class); Blade::component('hyde::breadcrumbs', BreadcrumbsComponent::class); } diff --git a/packages/framework/src/Framework/Services/OverwritePolicy.php b/packages/framework/src/Framework/Services/OverwritePolicy.php new file mode 100644 index 00000000000..185acf1b918 --- /dev/null +++ b/packages/framework/src/Framework/Services/OverwritePolicy.php @@ -0,0 +1,44 @@ +fail('Could not find an 404.md or 404.blade.php file!') - ->withTip('You can publish the default one using `php hyde publish:views`'); + ->withTip('You can publish the default one using `php hyde publish --page=404`'); } public function check_site_has_an_index_page(Result $result): Result @@ -74,7 +74,7 @@ public function check_site_has_an_index_page(Result $result): Result } return $result->fail('Could not find an index.md or index.blade.php file!') - ->withTip('You can publish the one of the built in templates using `php hyde publish:homepage`'); + ->withTip('You can publish the one of the built in templates using `php hyde publish --page`'); } public function check_documentation_site_has_an_index_page(Result $result): Result diff --git a/packages/framework/src/Framework/Services/ViewDiffService.php b/packages/framework/src/Framework/Services/ViewDiffService.php deleted file mode 100644 index 9ee1aa0f514..00000000000 --- a/packages/framework/src/Framework/Services/ViewDiffService.php +++ /dev/null @@ -1,57 +0,0 @@ - */ - public static function getViewFileHashIndex(): array - { - $filecache = []; - - foreach (glob(Hyde::vendorPath('resources/views/**/*.blade.php')) as $file) { - $filecache[unslash(str_replace(Hyde::vendorPath(), '', (string) $file))] = [ - 'unixsum' => unixsum_file($file), - ]; - } - - return $filecache; - } - - /** @return array */ - public static function getChecksums(): array - { - $checksums = []; - - foreach (static::getViewFileHashIndex() as $file) { - $checksums[] = $file['unixsum']; - } - - return $checksums; - } - - public static function checksumMatchesAny(string $checksum): bool - { - return in_array($checksum, static::getChecksums()); - } -} diff --git a/packages/framework/tests/Feature/Commands/PublishCommandPagesTest.php b/packages/framework/tests/Feature/Commands/PublishCommandPagesTest.php new file mode 100644 index 00000000000..8863485f642 --- /dev/null +++ b/packages/framework/tests/Feature/Commands/PublishCommandPagesTest.php @@ -0,0 +1,747 @@ +withoutDefaultPages(); + } + + protected function tearDown(): void + { + app()->forgetInstance(\Illuminate\Filesystem\Filesystem::class); + + ConsoleHelper::clearMocks(); + PagesPromptsReset::resetFallbacks(); + PublishablePages::clear(); + BladePage::setSourceDirectory('_pages'); + + foreach (glob(Hyde::path('_pages/*.blade.php')) as $file) { + if (File::isDirectory($file)) { + File::deleteDirectory($file); + } else { + File::delete($file); + } + } + + if (File::isDirectory(Hyde::path('_pages/company'))) { + File::deleteDirectory(Hyde::path('_pages/company')); + } + + if (File::isDirectory(Hyde::path('content'))) { + File::deleteDirectory(Hyde::path('content')); + } + + $this->restoreDefaultPages(); + + parent::tearDown(); + } + + public function testNamedPagePublishesToItsDefaultTargetNonInteractively() + { + $this->artisan('publish --page=welcome --no-interaction') + ->expectsOutputToContain('Published [welcome] to [_pages/index.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/index.blade.php')); + $this->assertSame( + File::get(Hyde::vendorPath('resources/views/homepages/welcome.blade.php')), + File::get(Hyde::path('_pages/index.blade.php')) + ); + } + + // --all means "all views" and does not apply to pages. Combined with --page, the page + // flow wins (page-intent is resolved before view-intent) and --all is inert โ€” it must not divert + // into the views "Published all" path. PagesPublisher never reads the --all option. + public function testAllFlagDoesNotApplyToPagesWhenCombinedWithPage() + { + $this->artisan('publish --page=welcome --all --no-interaction') + ->expectsOutputToContain('Published [welcome] to [_pages/index.blade.php]') + ->doesntExpectOutputToContain('Published all') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/index.blade.php')); + } + + public function testUnknownPageNameFailsHelpfully() + { + $this->artisan('publish --page=nope --no-interaction') + ->expectsOutputToContain('The page [nope] does not exist.') + ->expectsOutputToContain('Available pages: welcome, posts, blank, 404') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + // The '404' key is a string on the value object but coerces to an int array key: lookup must compare ->key. + public function testNumericPageKeyIsResolvedByItsStringKey() + { + $this->artisan('publish --page=404 --no-interaction') + ->expectsOutputToContain('Published [404] to [_pages/404.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/404.blade.php')); + } + + public function testToOverridesTheDefaultTarget() + { + $this->artisan('publish --page=posts --to=_pages/index.blade.php --no-interaction') + ->expectsOutputToContain('Published [posts] to [_pages/index.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/index.blade.php')); + $this->assertFileDoesNotExist(Hyde::path('_pages/posts.blade.php')); + } + + public function testPageWithoutDefaultTargetFailsNonInteractivelyWithoutTo() + { + $this->artisan('publish --page=blank --no-interaction') + ->expectsOutputToContain('The [blank] page has no default destination. Provide one with --to.') + ->assertExitCode(1); + } + + public function testPageWithoutDefaultTargetPublishesWithTo() + { + $this->artisan('publish --page=blank --to=_pages/about.blade.php --no-interaction') + ->expectsOutputToContain('Published [blank] to [_pages/about.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/about.blade.php')); + } + + public function testPageCanBePublishedToNestedCustomTarget() + { + $target = '_pages/company/about.blade.php'; + + $this->artisan("publish --page=blank --to=$target --no-interaction") + ->expectsOutputToContain("Published [blank] to [$target]") + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path($target)); + $this->assertSame( + File::get(Hyde::vendorPath('resources/views/homepages/blank.blade.php')), + File::get(Hyde::path($target)) + ); + } + + public function testDefaultTargetUsesConfiguredBladePageSourceDirectory() + { + BladePage::setSourceDirectory('content/pages'); + PublishablePages::clear(); + + $this->artisan('publish --page=welcome --no-interaction') + ->expectsOutputToContain('Published [welcome] to [content/pages/index.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('content/pages/index.blade.php')); + } + + public function testToPathOutsidePagesDirectoryIsRejected() + { + $this->artisan('publish --page=welcome --to=resources/views/foo.blade.php --no-interaction') + ->expectsOutputToContain('The --to path must be within _pages/ and end in .blade.php, for example _pages/index.blade.php.') + ->assertExitCode(1); + } + + public function testToPathWithWrongExtensionIsRejected() + { + $this->artisan('publish --page=welcome --to=_pages/index.md --no-interaction') + ->expectsOutputToContain('The --to path must be within _pages/ and end in .blade.php, for example _pages/index.blade.php.') + ->assertExitCode(1); + } + + public function testToPathIsValidatedAgainstThePublishablePageClass() + { + PublishablePages::register(new PublishablePage( + key: 'markdown', + label: 'Markdown page', + description: 'A Markdown starter page.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: null, + pageClass: PublishCommandPagesCustomPage::class, + )); + + $this->artisan('publish --page=markdown --to=content/custom/about.mdx --no-interaction') + ->expectsOutputToContain('Published [markdown] to [content/custom/about.mdx]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('content/custom/about.mdx')); + } + + public function testToPathWrongForThePublishablePageClassIsRejected() + { + PublishablePages::register(new PublishablePage( + key: 'markdown', + label: 'Markdown page', + description: 'A Markdown starter page.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: null, + pageClass: PublishCommandPagesCustomPage::class, + )); + + $this->artisan('publish --page=markdown --to=_pages/about.blade.php --no-interaction') + ->expectsOutputToContain('The --to path must be within content/custom/ and end in .mdx, for example content/custom/example.mdx.') + ->assertExitCode(1); + } + + public function testToIsRejectedForAPageThatDisallowsCustomTargets() + { + $this->artisan('publish --page=404 --to=_pages/error.blade.php --no-interaction') + ->expectsOutputToContain('The [404] page cannot be published to a custom path; omit --to to use its default (_pages/404.blade.php).') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/error.blade.php')); + } + + // The two --to rejections must never disagree. Bare --page + --to is a multi-select context with a single + // destination: the "one path can't serve several pages" guard (message A) must win over any per-page reason + // (message B, e.g. 404's custom-path rejection) AND fire before the picker โ€” so this interactive run, which + // would hang on an unanswered picker prompt if the picker were reached, asks nothing and exits on message A. + public function testBarePageWithToIsRejectedBeforeThePickerAndBeatsThePerPageReason() + { + $this->artisan('publish --page --to=_pages/error.blade.php') + ->expectsOutputToContain('--to is only valid when publishing a single page. Use --page=NAME with --to.') + ->doesntExpectOutputToContain('cannot be published to a custom path') + ->assertExitCode(1); + } + + public function testIdenticalPageIsSkippedAsAlreadyCurrent() + { + $this->artisan('publish --page=welcome --no-interaction')->assertExitCode(0); + + $this->artisan('publish --page=welcome --no-interaction') + ->expectsOutputToContain('All selected pages are already up to date.') + ->assertExitCode(0); + } + + public function testModifiedPageCannotBeOverwrittenNonInteractivelyWithoutForce() + { + File::put(Hyde::path('_pages/index.blade.php'), 'MODIFIED BY USER'); + + $this->artisan('publish --page=welcome --no-interaction') + ->expectsOutput('Cannot overwrite modified files without --force:') + ->expectsOutputToContain('_pages/index.blade.php') + ->expectsOutput('Run again with --force to overwrite.') + ->assertExitCode(1); + + $this->assertSame('MODIFIED BY USER', File::get(Hyde::path('_pages/index.blade.php'))); + } + + public function testForceOverwritesModifiedPage() + { + File::put(Hyde::path('_pages/index.blade.php'), 'MODIFIED BY USER'); + + $this->artisan('publish --page=welcome --force --no-interaction') + ->expectsOutputToContain('Published [welcome] to [_pages/index.blade.php]') + ->assertExitCode(0); + + $this->assertNotSame('MODIFIED BY USER', File::get(Hyde::path('_pages/index.blade.php'))); + } + + public function testPublishFailsWhenRegisteredPageSourceIsMissing() + { + $missingSource = 'resources/views/homepages/missing-source.blade.php'; + $target = '_pages/missing-source.blade.php'; + + PublishablePages::register(new PublishablePage( + key: 'missing-source', + label: 'Missing source page', + description: 'A page registered by an extension with a missing source file.', + source: $missingSource, + defaultTarget: $target, + )); + + $this->artisan('publish --page=missing-source --no-interaction') + ->expectsOutputToContain('Skipped ['.Hyde::path($target).']: source file ['.Hyde::vendorPath($missingSource).'] does not exist.') + ->doesntExpectOutputToContain('Published') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path($target)); + } + + public function testPublishFailsWhenRegisteredPageSourceIsADirectory() + { + $directorySource = 'resources/views/homepages'; + + PublishablePages::register(new PublishablePage( + key: 'directory-source', + label: 'Directory source page', + description: 'A page registered by an extension with a directory source.', + source: $directorySource, + defaultTarget: '_pages/directory-source.blade.php', + )); + + $this->artisan('publish --page=directory-source --no-interaction') + ->expectsOutputToContain('Skipped ['.Hyde::path('_pages/directory-source.blade.php').']: source ['.Hyde::vendorPath($directorySource).'] is not a file.') + ->doesntExpectOutputToContain('Published') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/directory-source.blade.php')); + } + + public function testPublishFailsWhenDestinationIsADirectory() + { + File::makeDirectory(Hyde::path('_pages/index.blade.php')); + + $this->artisan('publish --page=welcome --no-interaction') + ->expectsOutputToContain('Skipped ['.Hyde::path('_pages/index.blade.php').']: destination is a directory.') + ->doesntExpectOutputToContain('Published') + ->assertExitCode(1); + + $this->assertDirectoryExists(Hyde::path('_pages/index.blade.php')); + } + + public function testInvalidRegisteredPageDoesNotStopValidPagesFromPublishing() + { + $missingSource = 'resources/views/homepages/missing-source.blade.php'; + + PublishablePages::register(new PublishablePage( + key: 'missing-source', + label: 'Missing source page', + description: 'A page registered by an extension with a missing source file.', + source: $missingSource, + defaultTarget: '_pages/missing-source.blade.php', + )); + + PublishablePages::register(new PublishablePage( + key: 'about', + label: 'About page', + description: 'A valid page in the same batch.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: '_pages/about.blade.php', + )); + + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['missing-source', 'about']) + ->expectsConfirmation('Proceed?', 'yes') + ->expectsOutputToContain('Skipped ['.Hyde::path('_pages/missing-source.blade.php').']: source file ['.Hyde::vendorPath($missingSource).'] does not exist.') + ->expectsOutputToContain('Published [about] to [_pages/about.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/missing-source.blade.php')); + $this->assertFileExists(Hyde::path('_pages/about.blade.php')); + } + + public function testCopyFailureFailsWithoutReportingSuccess() + { + app()->instance(\Illuminate\Filesystem\Filesystem::class, new class extends \Illuminate\Filesystem\Filesystem + { + public function copy($path, $target): bool + { + return false; + } + }); + + $this->artisan('publish --page=welcome --no-interaction') + ->expectsOutputToContain('Error: Failed to copy') + ->doesntExpectOutputToContain('Published') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testInteractiveResolutionCanChooseAnAlternativeTarget() + { + $this->artisan('publish --page=posts') + ->expectsQuestion('Where should "Posts feed" be published?', '_pages/index.blade.php') + ->expectsOutputToContain('Published [posts] to [_pages/index.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/index.blade.php')); + $this->assertFileDoesNotExist(Hyde::path('_pages/posts.blade.php')); + } + + public function testInteractiveResolutionCanChooseACustomPath() + { + $this->artisan('publish --page=blank') + ->expectsQuestion('Where should "Blank page" be published?', '__hyde_custom_target__') + ->expectsQuestion('Enter a BladePage source path', '_pages/custom.blade.php') + ->expectsOutputToContain('Published [blank] to [_pages/custom.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/custom.blade.php')); + } + + public function testCustomPathFromPromptRepromptsUntilValid() + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + PagesPromptsReset::resetFallbacks(); + + $invalidPath = 'somewhere/else.blade.php'; + + Prompt::fake([ + Key::ENTER, + $invalidPath, + Key::ENTER, + ...array_fill(0, strlen($invalidPath), Key::BACKSPACE), + '_pages/custom.blade.php', + Key::ENTER, + Key::ENTER, + ]); + + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput(['--page' => 'blank'], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + $this->assertSame(0, $command->handle()); + + $this->assertFileExists(Hyde::path('_pages/custom.blade.php')); + $this->assertStringContainsString('Published [blank] to [_pages/custom.blade.php]', $output->fetch()); + Prompt::assertStrippedOutputContains('The path must be within _pages/ and end in .blade.php.'); + } + + public function testInteractivePickerPublishesSelectedPagesAfterConfirmation() + { + // Welcome has a single sensible destination, so it is not prompted for; it resolves to its default. + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome']) + ->expectsOutput('Ready to publish:') + ->expectsOutputToContain('Welcome page โ†’ _pages/index.blade.php') + ->expectsConfirmation('Proceed?', 'yes') + ->expectsOutputToContain('Published [welcome] to [_pages/index.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/index.blade.php')); + } + + public function testInteractivePickerCanBeDeclinedAtConfirmation() + { + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome']) + ->expectsConfirmation('Proceed?', 'no') + ->expectsOutputToContain('Cancelled. No pages were published.') + ->assertExitCode(0); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testEmptyPageSelectionExitsWithoutPublishing() + { + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput([], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + $publisher = new class(new PublisherConsole($command, $input)) extends PagesPublisher + { + protected function selectPages(): ?array + { + return []; + } + }; + + $this->assertSame(0, $publisher->publish()); + + $contents = $output->fetch(); + $this->assertStringContainsString('No pages selected; nothing to publish.', $contents); + $this->assertStringNotContainsString('Ready to publish:', $contents); + $this->assertStringNotContainsString('Published', $contents); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + $this->assertFileDoesNotExist(Hyde::path('_pages/404.blade.php')); + } + + public function testTwoPagesResolvingToTheSameTargetAreRejectedBeforeWriting() + { + PublishablePages::register(new PublishablePage( + key: 'clash', + label: 'Clashing page', + description: 'A page that targets the homepage too.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: '_pages/index.blade.php', + allowCustomTarget: false, + )); + + // Neither page is prompted for (welcome and clash each resolve straight to their default), so the + // collision is caught purely from the picker selection, before any destination prompt or write. + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome', 'clash']) + ->expectsOutputToContain('Welcome page and Clashing page both target _pages/index.blade.php.') + ->expectsOutputToContain('Pick one, or set --to for each.') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testCustomTargetWithDuplicateSlashesConflictsWithNormalizedDefaultTarget() + { + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome', 'blank']) + ->expectsQuestion('Where should "Blank page" be published?', '__hyde_custom_target__') + ->expectsQuestion('Enter a BladePage source path', '_pages//index.blade.php') + ->expectsOutputToContain('Welcome page and Blank page both target _pages/index.blade.php.') + ->expectsOutputToContain('Pick one, or set --to for each.') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testCustomTargetWithSingleDotSegmentConflictsWithNormalizedDefaultTarget() + { + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome', 'blank']) + ->expectsQuestion('Where should "Blank page" be published?', '__hyde_custom_target__') + ->expectsQuestion('Enter a BladePage source path', '_pages/./index.blade.php') + ->expectsOutputToContain('Welcome page and Blank page both target _pages/index.blade.php.') + ->expectsOutputToContain('Pick one, or set --to for each.') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testRebuildIsOfferedInteractivelyAfterPublishing() + { + // Welcome resolves to its default without a destination prompt, so the only interaction is the rebuild offer. + $this->artisan('publish --page=welcome') + ->expectsOutputToContain('Published [welcome] to [_pages/index.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + } + + public function testRebuildIsNeverOfferedNonInteractively() + { + $this->artisan('publish --page=welcome --no-interaction') + ->doesntExpectOutputToContain('Rebuild the site now?') + ->assertExitCode(0); + } + + // The picker round-trips the numeric '404' key: PHP coerces it to an int option key, and it must cast + // back to the string key to resolve. This covers the sneakier path the named --page=404 test cannot reach. + + public function testPickerCanSelectTheNumericKeyedPage() + { + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['404']) + ->expectsConfirmation('Proceed?', 'yes') + ->expectsOutputToContain('Published [404] to [_pages/404.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/404.blade.php')); + } + + public function testPickerDoesNotOfferAnAllRow() + { + // Space+enter selects the first row (welcome); the next enter accepts "Proceed?" (default yes), the + // last accepts "Rebuild the site now?" (default no) โ€” so the run completes without leftover prompts. + $output = $this->runPagesPicker([Key::SPACE, Key::ENTER, Key::ENTER, Key::ENTER]); + + Prompt::assertOutputContains('Select pages to publish'); + Prompt::assertOutputContains('Welcome page'); + Prompt::assertOutputDoesntContain('All pages'); + Prompt::assertOutputDoesntContain('All views'); + + // The first offered row is a real page (welcome), not a select-all sentinel, so a single space+enter publishes it. + $this->assertStringContainsString('Published [welcome]', $output->fetch()); + } + + // A bare --page (no name) needs the picker, which needs an interactive terminal, so non-interactively it fails. + public function testBarePageWithoutInteractionFailsHelpfully() + { + $this->artisan('publish --page --no-interaction') + ->expectsOutputToContain('No page specified for publishing. Provide one, for example --page=welcome.') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testThreePagesResolvingToTheSameTargetReportAllTarget() + { + foreach (['clash-one' => 'Clash One', 'clash-two' => 'Clash Two'] as $key => $label) { + PublishablePages::register(new PublishablePage( + key: $key, + label: $label, + description: 'A page that targets the homepage too.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: '_pages/index.blade.php', + allowCustomTarget: false, + )); + } + + // None of the three is prompted for (each resolves straight to _pages/index.blade.php), so the collision is + // caught from the picker selection alone, before any confirmation or write. + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome', 'clash-one', 'clash-two']) + ->expectsOutputToContain('Welcome page, Clash One and Clash Two all target _pages/index.blade.php.') + ->expectsOutputToContain('Pick one, or set --to for each.') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); + } + + public function testInteractiveConflictPromptCanOverwriteAPage() + { + File::put(Hyde::path('_pages/index.blade.php'), 'MODIFIED BY USER'); + + $this->artisan('publish --page=welcome') + ->expectsQuestion('1 selected files already exist and appear modified.', 'overwrite') + ->expectsOutputToContain('Published [welcome] to [_pages/index.blade.php]') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + + $this->assertNotSame('MODIFIED BY USER', File::get(Hyde::path('_pages/index.blade.php'))); + } + + public function testInteractiveConflictPromptCanSkipAModifiedPage() + { + File::put(Hyde::path('_pages/index.blade.php'), 'MODIFIED BY USER'); + + $this->artisan('publish --page=welcome') + ->expectsQuestion('1 selected files already exist and appear modified.', 'skip') + ->expectsOutputToContain('1 page left unchanged because they were modified:') + ->expectsOutputToContain('_pages/index.blade.php') + ->expectsOutputToContain('Run again with --force to overwrite.') + ->assertExitCode(0); + + // Skipping leaves the file as the user had it, and (nothing was written) never offers a rebuild. + $this->assertSame('MODIFIED BY USER', File::get(Hyde::path('_pages/index.blade.php'))); + } + + public function testInteractiveConflictPromptCanCancelForPages() + { + File::put(Hyde::path('_pages/index.blade.php'), 'MODIFIED BY USER'); + + $this->artisan('publish --page=welcome') + ->expectsQuestion('1 selected files already exist and appear modified.', 'cancel') + ->expectsOutputToContain('Cancelled. No pages were published.') + ->assertExitCode(0); + + $this->assertSame('MODIFIED BY USER', File::get(Hyde::path('_pages/index.blade.php'))); + } + + public function testMixedRunReportsPublishedAlongsideAlreadyCurrentPages() + { + // Seed two pages so they are already current, then register a third new page and publish all three. + $this->artisan('publish --page=welcome --no-interaction')->assertExitCode(0); + $this->artisan('publish --page=404 --no-interaction')->assertExitCode(0); + + PublishablePages::register(new PublishablePage( + key: 'about', + label: 'About page', + description: 'A simple about page.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: '_pages/about.blade.php', + )); + + // welcome and 404 are already current; only about is copied โ€” so the run reports both sides. + $this->artisan('publish --page') + ->expectsQuestion('Select pages to publish', ['welcome', '404', 'about']) + ->expectsConfirmation('Proceed?', 'yes') + ->expectsOutputToContain('Published [about] to [_pages/about.blade.php]') + ->expectsOutputToContain('2 pages already up to date and skipped.') + ->expectsConfirmation('Rebuild the site now?', 'no') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('_pages/about.blade.php')); + } + + // The command is driven directly (not through the console kernel) so that mocking the Artisan facade + // intercepts only maybeRebuild's own build call, rather than the runner's call that dispatches the command. + + public function testAcceptingTheRebuildOfferRunsTheBuild() + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + PagesPromptsReset::resetFallbacks(); + + Artisan::shouldReceive('call')->once()->with('build', [], \Mockery::any())->andReturn(0); + + // 'y' + enter answers the "Rebuild the site now?" confirm (which defaults to no) with yes. + Prompt::fake(['y', Key::ENTER]); + + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput(['--page' => 'welcome'], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + $this->assertSame(0, $command->handle()); + $this->assertStringContainsString('Published [welcome]', $output->fetch()); + } + + /** Drive the interactive pages picker with faked keystrokes and return the buffered output. */ + protected function runPagesPicker(array $keys): BufferedOutput + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + // Earlier --no-interaction runs in this class leave Prompt::$shouldFallback stuck true, which would + // route the picker through the (unrendered) fallback path; reset it so the prompt renders to the fake buffer. + PagesPromptsReset::resetFallbacks(); + + Prompt::fake($keys); + + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput(['--page' => null], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + $command->handle(); + + return $output; + } +} + +abstract class PagesPromptsReset extends Prompt +{ + // Workaround for https://github.com/laravel/prompts/issues/158 + public static function resetFallbacks(): void + { + static::$shouldFallback = false; + } +} + +class PublishCommandPagesCustomPage extends \Hyde\Pages\MarkdownPage +{ + public static string $sourceDirectory = 'content/custom'; + public static string $fileExtension = '.mdx'; +} diff --git a/packages/framework/tests/Feature/Commands/PublishCommandTest.php b/packages/framework/tests/Feature/Commands/PublishCommandTest.php new file mode 100644 index 00000000000..c6c3978353b --- /dev/null +++ b/packages/framework/tests/Feature/Commands/PublishCommandTest.php @@ -0,0 +1,251 @@ +restoreIndexPage) { + if ($this->originalIndexPage === null) { + File::delete(Hyde::path('_pages/index.blade.php')); + } else { + File::put(Hyde::path('_pages/index.blade.php'), $this->originalIndexPage); + } + } + + parent::tearDown(); + } + + public function testTagFlagIsRedirectedToVendorPublish() + { + $this->artisan('publish --tag=foo') + ->expectsOutputToContain('Use php hyde vendor:publish --tag=foo for tag/provider publishing.') + ->assertExitCode(1); + } + + public function testBareTagFlagIsRedirectedToVendorPublish() + { + $this->artisan('publish --tag') + ->expectsOutputToContain('Use php hyde vendor:publish --tag for tag/provider publishing.') + ->assertExitCode(1); + } + + public function testProviderFlagIsRedirectedToVendorPublish() + { + $this->artisan('publish --provider=FooServiceProvider') + ->expectsOutputToContain('Use php hyde vendor:publish --provider=FooServiceProvider for tag/provider publishing.') + ->assertExitCode(1); + } + + public function testConfigFlagIsRedirectedToVendorPublish() + { + $this->artisan('publish --config') + ->expectsOutputToContain('Config is not published through this command. Use php hyde vendor:publish --tag=hyde-config instead.') + ->assertExitCode(1); + } + + public function testLayoutsAndComponentsAreMutuallyExclusive() + { + $this->artisan('publish --layouts --components') + ->expectsOutputToContain('The --layouts and --components options are mutually exclusive. Use --all to publish both.') + ->assertExitCode(1); + } + + public function testToOptionRequiresThePageFlag() + { + $this->artisan('publish --to=_pages/index.blade.php') + ->expectsOutputToContain('--to is only valid when publishing a page.') + ->assertExitCode(1); + } + + public function testToOptionRequiresANamedPageNotABarePageFlag() + { + $this->artisan('publish --page --to=_pages/index.blade.php') + ->expectsOutputToContain('--to is only valid when publishing a single page. Use --page=NAME with --to.') + ->assertExitCode(1); + } + + public function testNonInteractiveWithNoActionableFlagsFailsWithUsageHint() + { + $this->artisan('publish --no-interaction') + ->expectsOutput('Nothing to publish. Try:') + ->expectsOutput(' php hyde publish --all') + ->expectsOutput(' php hyde publish --layouts') + ->expectsOutput(' php hyde publish --page=welcome') + ->assertExitCode(1); + } + + public function testLayoutsFlagRoutesToViews() + { + $this->artisan('publish --layouts --no-interaction') + ->expectsOutputToContain('views to [resources/views/vendor/hyde/layouts]') + ->assertExitCode(0); + } + + public function testComponentsFlagRoutesToViews() + { + $this->artisan('publish --components --no-interaction') + ->expectsOutputToContain('views to [resources/views/vendor/hyde/components]') + ->assertExitCode(0); + } + + public function testAllFlagRoutesToViews() + { + $this->artisan('publish --all --no-interaction') + ->expectsOutputToContain('Published all') + ->assertExitCode(0); + } + + public function testBarePageFlagRoutesToPages() + { + $this->artisan('publish --page --no-interaction') + ->expectsOutputToContain('No page specified for publishing. Provide one, for example --page=welcome.') + ->assertExitCode(1); + } + + public function testPageFlagWithEmptyValueFailsBeforeTheWizard() + { + $this->artisan('publish --page= --no-interaction') + ->expectsOutputToContain('The --page option cannot be empty. Use --page for the picker or --page=welcome.') + ->assertExitCode(1); + } + + public function testPageFlagWithNameRoutesToPages() + { + $this->artisan('publish --page=nonexistent --no-interaction') + ->expectsOutputToContain('The page [nonexistent] does not exist.') + ->assertExitCode(1); + } + + public function testProgrammaticBarePageFlagRoutesToPages() + { + $this->assertSame(1, Artisan::call('publish', [ + '--page' => null, + '--no-interaction' => true, + ])); + + $this->assertStringContainsString( + 'No page specified for publishing. Provide one, for example --page=welcome.', + Artisan::output() + ); + } + + public function testProgrammaticEmptyPageFlagFailsBeforeTheWizard() + { + $this->assertSame(1, Artisan::call('publish', [ + '--page' => '', + '--no-interaction' => true, + ])); + + $this->assertStringContainsString( + 'The --page option cannot be empty. Use --page for the picker or --page=welcome.', + Artisan::output() + ); + } + + public function testProgrammaticPageNameRoutesToPages() + { + $this->assertSame(1, Artisan::call('publish', [ + '--page' => 'nonexistent', + '--no-interaction' => true, + ])); + + $this->assertStringContainsString('The page [nonexistent] does not exist.', Artisan::output()); + } + + public function testWizardRoutesToViews() + { + $appLayout = (is_dir(Hyde::path('packages')) ? 'packages' : 'vendor/hyde').'/framework/resources/views/layouts/app.blade.php'; + + $this->artisan('publish') + ->expectsQuestion('What do you want to publish?', 'views') + ->expectsQuestion('Select Hyde views to publish', [$appLayout]) + ->expectsOutputToContain('Published 1 view') + ->assertExitCode(0); + } + + public function testWizardRoutesToPages() + { + // Route through the wizard into the real pages flow. A deliberately modified target makes the overwrite + // guard prove the wizard reached PagesPublisher without depending on the repository fixture contents. + $this->modifyDefaultHomePage(); + + $this->artisan('publish') + ->expectsQuestion('What do you want to publish?', 'page') + ->expectsQuestion('Select pages to publish', ['welcome']) + ->expectsConfirmation('Proceed?', 'yes') + ->expectsQuestion('1 selected files already exist and appear modified.', 'skip') + ->expectsOutputToContain('1 page left unchanged because they were modified:') + ->assertExitCode(0); + } + + protected function modifyDefaultHomePage(): void + { + $target = Hyde::path('_pages/index.blade.php'); + + $this->restoreIndexPage = true; + $this->originalIndexPage = File::exists($target) ? File::get($target) : null; + + File::put($target, 'MODIFIED BY USER'); + } + + public function testWizardCancelExitsCleanlyWithoutPublishing() + { + $this->artisan('publish') + ->expectsQuestion('What do you want to publish?', 'cancel') + ->doesntExpectOutputToContain('Published') + ->assertExitCode(0); + } + + public function testUnknownOptionIsNotSwallowed() + { + // A typo for --layouts must surface Symfony's native error, not be eaten by our + // raw-flag interception (which only short-circuits --tag/--provider/--config). + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The "--layout" option does not exist.'); + + $this->artisan('publish --layout')->run(); + } + + public function testArbitrarySourcePathArgumentIsRejected() + { + // The command declares no arguments, so a stray source path is rejected outright + // rather than being interpreted as a publishable target. + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No arguments expected for "publish" command, got "resources/views/foo.blade.php".'); + + $this->artisan('publish resources/views/foo.blade.php')->run(); + } + + // The legacy publish commands are removed in v3, not aliased. Invoking one must raise Symfony's + // native command-not-found error, proving the command is gone and that no shim intercepts it. + + public function testRemovedLegacyPublishViewsCommandRaisesCommandNotFound() + { + $this->expectException(CommandNotFoundException::class); + $this->expectExceptionMessage('The command "publish:views" does not exist.'); + + $this->artisan('publish:views')->run(); + } +} diff --git a/packages/framework/tests/Feature/Commands/PublishCommandViewsTest.php b/packages/framework/tests/Feature/Commands/PublishCommandViewsTest.php new file mode 100644 index 00000000000..0efd83035d3 --- /dev/null +++ b/packages/framework/tests/Feature/Commands/PublishCommandViewsTest.php @@ -0,0 +1,470 @@ +viewCount('layouts') + $this->viewCount('components'); + + $this->artisan('publish --all --no-interaction') + ->expectsOutputToContain("Published all $count views to [resources/views/vendor/hyde]") + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + } + + public function testLayoutsPublishesOnlyLayoutsNonInteractively() + { + $count = $this->viewCount('layouts'); + + $this->artisan('publish --layouts --no-interaction') + ->expectsOutputToContain("Published all $count views to [resources/views/vendor/hyde/layouts]") + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + } + + public function testComponentsPublishesOnlyComponentsNonInteractively() + { + $count = $this->viewCount('components'); + + $this->artisan('publish --components --no-interaction') + ->expectsOutputToContain("Published all $count views to [resources/views/vendor/hyde/components]") + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts')); + } + + public function testPickerCanPublishASingleView() + { + $this->artisan('publish --layouts') + ->expectsQuestion('Select Hyde views to publish', [$this->source('layouts', 'app.blade.php')]) + ->expectsOutputToContain('Published 1 view to [resources/views/vendor/hyde/layouts/app.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); + } + + public function testEmptyViewSelectionExitsWithoutPublishing() + { + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput([], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + $publisher = new class(new PublisherConsole($command, $input)) extends ViewsPublisher + { + protected function selectFiles(array $offered, array $labels): array + { + return []; + } + }; + + $this->assertSame(0, $publisher->publish()); + + $contents = $output->fetch(); + $this->assertStringContainsString('No views selected; nothing to publish.', $contents); + $this->assertStringNotContainsString('Published', $contents); + + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde')); + } + + public function testPickerCanPublishManyViewsFromOneGroup() + { + $this->artisan('publish --layouts') + ->expectsQuestion('Select Hyde views to publish', [ + $this->source('layouts', 'app.blade.php'), + $this->source('layouts', 'page.blade.php'), + $this->source('layouts', 'post.blade.php'), + ]) + ->expectsOutputToContain('Published 3 views to [resources/views/vendor/hyde/layouts]') + ->assertExitCode(0); + } + + public function testPickerBaseDirectorySpansGroupsWhenBothAreSelected() + { + $this->artisan('publish') + ->expectsQuestion('What do you want to publish?', 'views') + ->expectsQuestion('Select Hyde views to publish', [ + $this->source('layouts', 'app.blade.php'), + $this->source('components', 'article-excerpt.blade.php'), + ]) + ->expectsOutputToContain('Published 2 views to [resources/views/vendor/hyde]') + ->assertExitCode(0); + } + + public function testBaseDirectoryIgnoresSharedSegmentsAfterPathsDiverge() + { + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput([], $command->getDefinition()); + $publisher = new class(new PublisherConsole($command, $input)) extends ViewsPublisher + { + public function exposeBaseDirectory(array $files): string + { + return $this->baseDirectory($files); + } + }; + + $this->assertSame('resources/views/vendor/hyde', $publisher->exposeBaseDirectory([ + 'resources/views/vendor/hyde/layouts/page.blade.php', + 'resources/views/vendor/hyde/components/page.blade.php', + ])); + } + + public function testLayoutsPickerIsPrefilteredWithGroupPrefixedLabels() + { + $output = $this->runViewsPicker(['--layouts' => true], [Key::SPACE, Key::ENTER]); + + Prompt::assertOutputContains('Select Hyde views to publish'); + Prompt::assertOutputContains('All views'); + Prompt::assertOutputContains('layouts/app.blade.php'); + Prompt::assertOutputDoesntContain('components/'); + + // Checking the "All views" sentinel selects every offered (layouts) view. + $this->assertStringContainsString('Published all', $output->fetch()); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + } + + public function testComponentsPickerIsPrefiltered() + { + $this->runViewsPicker(['--components' => true], [Key::SPACE, Key::ENTER]); + + Prompt::assertOutputContains('components/article-excerpt.blade.php'); + Prompt::assertOutputDoesntContain('layouts/'); + } + + public function testAllSentinelPreservesNumericOptionKeys() + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + Prompt::fake([Key::DOWN, Key::SPACE, Key::ENTER]); + + $this->assertSame([404], InteractiveMultiselect::select('Select test option', [404 => 'Not found'], 'All options')); + } + + public function testPickerCanSelectNumericOptionKeyDirectly() + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + Prompt::fake([Key::SPACE, Key::ENTER]); + + $this->assertSame([404], InteractiveMultiselect::select('Select test option', [404 => 'Not found'])); + } + + public function testPickerCanSubmitAnEmptySelection() + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + Prompt::fake([Key::ENTER]); + + $this->assertSame([], InteractiveMultiselect::select('Select test option', ['one' => 'One'])); + } + + public function testIdenticalViewsAreSkippedAsAlreadyCurrent() + { + $this->seedAllViews(); + + $this->artisan('publish --all --no-interaction') + ->expectsOutputToContain('All selected views are already up to date.') + ->assertExitCode(0); + } + + public function testModifiedViewsCannotBeOverwrittenNonInteractivelyWithoutForce() + { + $this->seedAllViews(); + $target = $this->modifyPublishedView(); + + $this->artisan('publish --all --no-interaction') + ->expectsOutput('Cannot overwrite modified files without --force:') + ->expectsOutputToContain('resources/views/vendor/hyde/layouts/app.blade.php') + ->expectsOutput('Run again with --force to overwrite.') + ->assertExitCode(1); + + // Hard stop: the modified file is left untouched and nothing else is written either. + $this->assertSame('MODIFIED BY USER', File::get($target)); + } + + public function testForceOverwritesModifiedViews() + { + $this->seedAllViews(); + $target = $this->modifyPublishedView(); + + $this->artisan('publish --all --force --no-interaction') + ->assertExitCode(0); + + $this->assertNotSame('MODIFIED BY USER', File::get($target)); + $this->assertSame(File::get(Hyde::path($this->source('layouts', 'app.blade.php'))), File::get($target)); + } + + public function testCopyFailureFailsWithoutReportingSuccess() + { + app()->instance(\Illuminate\Filesystem\Filesystem::class, new class extends \Illuminate\Filesystem\Filesystem + { + public function copy($path, $target): bool + { + return false; + } + }); + + $this->artisan('publish --layouts --no-interaction') + ->expectsOutputToContain('Error: Failed to copy') + ->doesntExpectOutputToContain('Published') + ->assertExitCode(1); + + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + } + + public function testInvalidViewDoesNotStopValidViewsFromPublishing() + { + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput([], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + $validSource = $this->source('layouts', 'app.blade.php'); + $missingSource = $this->source('layouts', 'missing-view.blade.php'); + + $publisher = new class(new PublisherConsole($command, $input), $validSource, $missingSource) extends ViewsPublisher + { + public function __construct(PublisherConsole $console, protected string $validSource, protected string $missingSource) + { + parent::__construct($console); + } + + protected function collectOfferedFiles(): array + { + return [ + [ + $this->missingSource => 'resources/views/vendor/hyde/layouts/missing-view.blade.php', + $this->validSource => 'resources/views/vendor/hyde/layouts/app.blade.php', + ], + [ + $this->missingSource => 'layouts/missing-view.blade.php', + $this->validSource => 'layouts/app.blade.php', + ], + ]; + } + + protected function selectFiles(array $offered, array $labels): array + { + return array_keys($offered); + } + }; + + $this->assertSame(1, $publisher->publish()); + + $contents = $output->fetch(); + $this->assertStringContainsString('Skipped [resources/views/vendor/hyde/layouts/missing-view.blade.php]: source file ['.$missingSource.'] does not exist.', $contents); + $this->assertStringContainsString('Published 1 view to [resources/views/vendor/hyde/layouts/app.blade.php]', $contents); + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/missing-view.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + } + + public function testUnclassifiedInvalidViewStateIsReported() + { + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput([], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + $source = $this->source('layouts', 'app.blade.php'); + $target = 'resources/views/vendor/hyde/layouts/app.blade.php'; + + $publisher = new class(new PublisherConsole($command, $input), $source, $target) extends ViewsPublisher + { + public function __construct(PublisherConsole $console, protected string $source, protected string $target) + { + parent::__construct($console); + } + + protected function collectOfferedFiles(): array + { + return [[$this->source => $this->target], [$this->source => 'layouts/app.blade.php']]; + } + + protected function selectFiles(array $offered, array $labels): array + { + return array_keys($offered); + } + + protected function decide(array $selected): array + { + $this->reportPolicyError($this->console, $this->source, $this->target); + + return [[], [], []]; + } + }; + + $this->assertSame(1, $publisher->publish()); + + $this->assertStringContainsString( + 'Skipped [resources/views/vendor/hyde/layouts/app.blade.php]: source or destination is invalid.', + $output->fetch() + ); + } + + public function testInteractiveConflictPromptCanOverwrite() + { + $this->seedAllViews(); + $target = $this->modifyPublishedView(); + + $this->artisan('publish --all') + ->expectsQuestion('1 selected files already exist and appear modified.', 'overwrite') + ->expectsOutputToContain('Published 1 view to [resources/views/vendor/hyde/layouts/app.blade.php]') + ->assertExitCode(0); + + $this->assertNotSame('MODIFIED BY USER', File::get($target)); + } + + public function testInteractiveConflictPromptCanSkip() + { + $this->seedAllViews(); + $target = $this->modifyPublishedView(); + + $this->artisan('publish --all') + ->expectsQuestion('1 selected files already exist and appear modified.', 'skip') + ->expectsOutputToContain('left unchanged because they were modified') + ->assertExitCode(0); + + $this->assertSame('MODIFIED BY USER', File::get($target)); + } + + public function testInteractiveConflictPromptCanCancel() + { + $this->seedAllViews(); + $target = $this->modifyPublishedView(); + + $this->artisan('publish --all') + ->expectsQuestion('1 selected files already exist and appear modified.', 'cancel') + ->expectsOutputToContain('Cancelled. No views were published.') + ->assertExitCode(0); + + $this->assertSame('MODIFIED BY USER', File::get($target)); + } + + public function testMixedRunReportsPublishedAlongsideAlreadyCurrentViews() + { + // Seed only the layouts so they are already current, then publish everything: components copy, layouts skip. + $this->artisan('publish --layouts --no-interaction')->assertExitCode(0); + + $components = $this->viewCount('components'); + $layouts = $this->viewCount('layouts'); + + $this->artisan('publish --all --no-interaction') + ->expectsOutputToContain("Published $components views to [resources/views/vendor/hyde/components]") + ->expectsOutputToContain("$layouts views already up to date and skipped.") + ->doesntExpectOutputToContain('Published all') + ->assertExitCode(0); + } + + protected function viewCount(string $group): int + { + return Filesystem::findFiles("packages/framework/resources/views/$group", '.blade.php', true)->count(); + } + + protected function source(string $group, string $file): string + { + return (is_dir(Hyde::path('packages')) ? 'packages' : 'vendor/hyde')."/framework/resources/views/$group/$file"; + } + + /** Publish every view so subsequent runs see identical (already current) destinations. */ + protected function seedAllViews(): void + { + $this->artisan('publish --all --no-interaction')->assertExitCode(0); + } + + /** Modify one already-published view so it is seen as user-modified, and return its target path. */ + protected function modifyPublishedView(): string + { + $target = Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php'); + File::put($target, 'MODIFIED BY USER'); + + return $target; + } + + /** Drive the interactive picker with faked keystrokes and return the buffered output. */ + protected function runViewsPicker(array $parameters, array $keys): BufferedOutput + { + if (windows_os()) { + $this->markTestSkipped('Interactive prompts are not applicable on Windows systems.'); + } + + Prompt::fake($keys); + + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput($parameters, $command->getDefinition()); + $output = new BufferedOutput(); + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + $command->handle(); + + return $output; + } + + protected function tearDown(): void + { + app()->forgetInstance(\Illuminate\Filesystem\Filesystem::class); + + ConsoleHelper::clearMocks(); + ViewsPromptsReset::resetFallbacks(); + + if (File::isDirectory(Hyde::path('resources/views/vendor'))) { + File::deleteDirectory(Hyde::path('resources/views/vendor')); + } + + parent::tearDown(); + } +} + +abstract class ViewsPromptsReset extends Prompt +{ + // Workaround for https://github.com/laravel/prompts/issues/158 + public static function resetFallbacks(): void + { + static::$shouldFallback = false; + } +} diff --git a/packages/framework/tests/Feature/Commands/PublishConfigsCommandTest.php b/packages/framework/tests/Feature/Commands/PublishConfigsCommandTest.php deleted file mode 100644 index 3a9efbca14c..00000000000 --- a/packages/framework/tests/Feature/Commands/PublishConfigsCommandTest.php +++ /dev/null @@ -1,72 +0,0 @@ -artisan('publish:configs') - ->expectsChoice('Which configuration files do you want to publish?', 'All configs', $this->expectedOptions()) - ->expectsOutput(sprintf('Published config files to [%s]', Hyde::path('config'))) - ->assertExitCode(0); - } - - public function testConfigFilesArePublished() - { - $this->assertDirectoryDoesNotExist(Hyde::path('config')); - - $this->artisan('publish:configs') - ->expectsChoice('Which configuration files do you want to publish?', 'All configs', $this->expectedOptions()) - ->assertExitCode(0); - - $this->assertFileEquals(Hyde::vendorPath('config/hyde.php'), Hyde::path('config/hyde.php')); - - $this->assertDirectoryExists(Hyde::path('config')); - } - - public function testCommandOverwritesExistingFiles() - { - File::makeDirectory(Hyde::path('config')); - File::put(Hyde::path('config/hyde.php'), 'foo'); - - $this->artisan('publish:configs') - ->expectsChoice('Which configuration files do you want to publish?', 'All configs', $this->expectedOptions()) - ->assertExitCode(0); - - $this->assertNotSame('foo', File::get(Hyde::path('config/hyde.php'))); - } - - protected function expectedOptions(): array - { - return [ - 'All configs', - 'hyde-configs: Main configuration files', - 'support-configs: Laravel and package configuration files', - ]; - } -} diff --git a/packages/framework/tests/Feature/Commands/PublishHomepageCommandTest.php b/packages/framework/tests/Feature/Commands/PublishHomepageCommandTest.php deleted file mode 100644 index e5fad845ea0..00000000000 --- a/packages/framework/tests/Feature/Commands/PublishHomepageCommandTest.php +++ /dev/null @@ -1,124 +0,0 @@ -withoutDefaultPages(); - } - - protected function tearDown(): void - { - if (Filesystem::exists('_pages/index.blade.php')) { - Filesystem::unlink('_pages/index.blade.php'); - } - - $this->restoreDefaultPages(); - - parent::tearDown(); - } - - public function testThereAreNoDefaultPages() - { - $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandReturnsExpectedOutput() - { - $this->artisan('publish:homepage welcome') - ->expectsConfirmation('Would you like to rebuild the site?') - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandReturnsExpectedOutputWithRebuild() - { - $this->artisan('publish:homepage welcome') - ->expectsConfirmation('Would you like to rebuild the site?', 'yes') - ->expectsOutput('Okay, building site!') - ->expectsOutput('Site is built!') - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - $this->resetSite(); - } - - public function testCommandPromptsForOutput() - { - $this->artisan('publish:homepage') - ->expectsQuestion( - 'Which homepage do you want to publish?', - 'welcome: The default welcome page.' - ) - ->expectsOutput('Published page [welcome]') - ->expectsConfirmation('Would you like to rebuild the site?') - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandShowsFeedbackOutputWhenSupplyingAHomepageName() - { - $this->artisan('publish:homepage welcome') - ->expectsOutput('Published page [welcome]') - ->expectsConfirmation('Would you like to rebuild the site?', false) - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandHandlesErrorCode404() - { - $this->artisan('publish:homepage invalid-page') - ->assertExitCode(404); - - $this->assertFileDoesNotExist(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandDoesNotOverwriteModifiedFilesWithoutForceFlag() - { - file_put_contents(Hyde::path('_pages/index.blade.php'), 'foo'); - - $this->artisan('publish:homepage welcome') - ->assertExitCode(409); - - $this->assertSame('foo', file_get_contents(Hyde::path('_pages/index.blade.php'))); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandOverwritesModifiedFilesIfForceFlagIsSet() - { - file_put_contents(Hyde::path('_pages/index.blade.php'), 'foo'); - - $this->artisan('publish:homepage welcome --force --no-interaction') - ->assertExitCode(0); - - $this->assertNotSame('foo', file_get_contents(Hyde::path('_pages/index.blade.php'))); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - } - - public function testCommandDoesNotReturn409IfTheCurrentFileIsADefaultFile() - { - copy(Hyde::vendorPath('resources/views/layouts/app.blade.php'), Hyde::path('_pages/index.blade.php')); - - $this->artisan('publish:homepage welcome --no-interaction') - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_pages/index.blade.php')); - } -} diff --git a/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php b/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php deleted file mode 100644 index 7ba3109fcca..00000000000 --- a/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php +++ /dev/null @@ -1,260 +0,0 @@ -count() - + Filesystem::findFiles('vendor/hyde/framework/resources/views/layouts', '.blade.php', true)->count(); - - $this->artisan('publish:views') - ->expectsQuestion('Which group do you want to publish?', 'all') - ->doesntExpectOutputToContain('Selected group') - ->expectsOutput("Published all $count files to [resources/views/vendor/hyde]") - ->assertExitCode(0); - - // Assert all groups were published - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde')); - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/layouts')); - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/components')); - - // Assert files were published - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - - // Assert subdirectories were published with files - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/components/docs')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/components/docs/documentation-article.blade.php')); - } - - public function testCanSelectGroupWithArgument() - { - ConsoleHelper::disableLaravelPrompts(); - - $this->artisan('publish:views layouts') - ->expectsOutput('Published all [layout] files to [resources/views/vendor/hyde/layouts]') - ->assertExitCode(0); - - // Assert selected group was published - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde')); - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/layouts')); - - // Assert files were published - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); - - // Assert not selected group was not published - $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - } - - public function testCanSelectGroupWithQuestion() - { - ConsoleHelper::disableLaravelPrompts(); - - $this->artisan('publish:views') - ->expectsQuestion('Which group do you want to publish?', 'layouts: Shared layout views, such as the app layout, navigation menu, and Markdown page templates') - ->expectsOutput('Published all [layout] files to [resources/views/vendor/hyde/layouts]') - ->assertExitCode(0); - - // Assert selected group was published - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde')); - $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/layouts')); - - // Assert files were published - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); - - // Assert not selected group was not published - $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - } - - public function testWithInvalidSuppliedTag() - { - $this->artisan('publish:views invalid') - ->expectsOutput("Invalid selection: 'invalid'") - ->expectsOutput('Allowed values are: [all, layouts, components]') - ->assertExitCode(1); - } - - public function testInteractiveSelectionOnWindowsSystemsSkipsInteractiveness() - { - ConsoleHelper::mockWindowsOs(true); - - $this->artisan('publish:views components') - ->expectsOutput('Published all [component] files to [resources/views/vendor/hyde/components]') - ->assertExitCode(0); - } - - public function testInteractiveSelectionOnUnixSystems() - { - if (windows_os()) { - $this->markTestSkipped('Test is not applicable on Windows systems.'); - } - - Prompt::fake([ - Key::DOWN, Key::SPACE, - Key::DOWN, Key::SPACE, - Key::DOWN, Key::SPACE, - Key::ENTER, - ]); - - $output = $this->executePublishViewsCommand(); - - Prompt::assertOutputContains('Select the files you want to publish'); - Prompt::assertOutputContains('All files'); - Prompt::assertOutputContains('app.blade.php'); - Prompt::assertOutputContains('docs.blade.php'); - Prompt::assertOutputContains('footer.blade.php'); - - $this->assertSame("Published selected [layout] files to [resources/views/vendor/hyde/layouts]\n", $output->fetch()); - - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/docs.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/footer.blade.php')); - - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); - } - - public function testInteractiveSelectionWithHittingEnterRightAway() - { - if (windows_os()) { - $this->markTestSkipped('Test is not applicable on Windows systems.'); - } - - Prompt::fake([ - Key::ENTER, - ]); - - $output = $this->executePublishViewsCommand(); - - Prompt::assertOutputContains('Select the files you want to publish'); - Prompt::assertOutputContains('All files'); - Prompt::assertOutputContains('app.blade.php'); - Prompt::assertOutputContains('docs.blade.php'); - - $this->assertSame("Published all [layout] files to [resources/views/vendor/hyde/layouts]\n", $output->fetch()); - - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/docs.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/footer.blade.php')); - - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); - } - - public function testInteractiveSelectionWithComplexToggles() - { - if (windows_os()) { - $this->markTestSkipped('Test is not applicable on Windows systems.'); - } - - Prompt::fake([ - // Select "all files" - Key::SPACE, - // Unselect next file - Key::DOWN, Key::SPACE, - // Go back up and deselect the all files option - Key::UP, Key::SPACE, - // Select the next three files - Key::DOWN, Key::SPACE, Key::DOWN, Key::SPACE, Key::DOWN, Key::SPACE, - // De-select the last file - Key::SPACE, - // Confirm selection - Key::ENTER, - ]); - - $output = $this->executePublishViewsCommand(); - - Prompt::assertOutputContains('Select the files you want to publish'); - Prompt::assertOutputContains('All files'); - Prompt::assertOutputContains('app.blade.php'); - Prompt::assertOutputContains('docs.blade.php'); - - $this->assertSame("Published selected [layout] files to [resources/views/vendor/hyde/layouts]\n", $output->fetch()); - - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/docs.blade.php')); - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/footer.blade.php')); - - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); - } - - public function testCanSelectGroupWithQuestionAndPrompts() - { - if (windows_os()) { - $this->markTestSkipped('Test is not applicable on Windows systems.'); - } - - $this->artisan('publish:views') - ->expectsQuestion('Which group do you want to publish?', 'layouts: Shared layout views, such as the app layout, navigation menu, and Markdown page templates') - ->expectsOutput('Selected group [layouts]') - ->expectsQuestion('Select the files you want to publish', [(is_dir(Hyde::path('packages')) ? 'packages' : 'vendor/hyde').'/framework/resources/views/layouts/app.blade.php']) - ->expectsOutput('Published selected file to [resources/views/vendor/hyde/layouts/app.blade.php]') - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); - - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); - $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); - $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - } - - protected function executePublishViewsCommand(): BufferedOutput - { - $command = (new PublishViewsCommand()); - $input = new ArrayInput(['group' => 'layouts'], $command->getDefinition()); - $output = new BufferedOutput(); - $command->setInput($input); - $command->setOutput(new OutputStyle($input, $output)); - $command->handle(); - - return $output; - } - - protected function tearDown(): void - { - ConsoleHelper::clearMocks(); - PromptsReset::resetFallbacks(); - - if (File::isDirectory(Hyde::path('resources/views/vendor'))) { - File::deleteDirectory(Hyde::path('resources/views/vendor')); - } - - parent::tearDown(); - } -} - -abstract class PromptsReset extends Prompt -{ - // Workaround for https://github.com/laravel/prompts/issues/158 - public static function resetFallbacks(): void - { - static::$shouldFallback = false; - } -} diff --git a/packages/framework/tests/Feature/Commands/PublisherConsoleTest.php b/packages/framework/tests/Feature/Commands/PublisherConsoleTest.php new file mode 100644 index 00000000000..fc5ea83f758 --- /dev/null +++ b/packages/framework/tests/Feature/Commands/PublisherConsoleTest.php @@ -0,0 +1,141 @@ +makeConsole(); + + $this->assertSame([], $console->resolveBlocked([], 'Cancelled. No files were published.')); + $this->assertSame('', $output->fetch()); + } + + public function testForceAllowsModifiedFilesToBeOverwrittenWithoutPrompting(): void + { + $blocked = [$this->blockedFile('resources/views/vendor/hyde/layouts/app.blade.php')]; + [$console, $output] = $this->makeConsole(['--force' => true]); + + $this->assertSame($blocked, $console->resolveBlocked($blocked, 'Cancelled. No files were published.')); + $this->assertSame('', $output->fetch()); + } + + public function testNonInteractiveRunRefusesToOverwriteModifiedFilesWithoutForce(): void + { + $blocked = [ + $this->blockedFile('resources/views/vendor/hyde/layouts/app.blade.php'), + $this->blockedFile('_pages/index.blade.php'), + ]; + [$console, $output] = $this->makeConsole(); + + $this->assertNull($console->resolveBlocked($blocked, 'Cancelled. No files were published.')); + + $contents = $output->fetch(); + $this->assertStringContainsString('Cannot overwrite modified files without --force:', $contents); + $this->assertStringContainsString('resources/views/vendor/hyde/layouts/app.blade.php', $contents); + $this->assertStringContainsString('_pages/index.blade.php', $contents); + $this->assertStringContainsString('Run again with --force to overwrite.', $contents); + } + + public function testInteractiveRunCanSkipModifiedFiles(): void + { + $blocked = [$this->blockedFile('_pages/index.blade.php')]; + [$console, $output] = $this->makeInteractiveConsole([Key::ENTER]); + + $this->assertSame([], $console->resolveBlocked($blocked, 'Cancelled. No pages were published.')); + $this->assertSame('', $output->fetch()); + + Prompt::assertOutputContains('1 selected files already exist and appear modified.'); + Prompt::assertOutputContains('Skip modified files'); + } + + public function testInteractiveRunCanOverwriteModifiedFiles(): void + { + $blocked = [$this->blockedFile('resources/views/vendor/hyde/layouts/app.blade.php')]; + [$console, $output] = $this->makeInteractiveConsole([Key::DOWN, Key::ENTER]); + + $this->assertSame($blocked, $console->resolveBlocked($blocked, 'Cancelled. No views were published.')); + $this->assertSame('', $output->fetch()); + } + + public function testInteractiveRunCanCancelBeforeOverwritingModifiedFiles(): void + { + $blocked = [$this->blockedFile('_pages/index.blade.php')]; + [$console, $output] = $this->makeInteractiveConsole([Key::DOWN, Key::DOWN, Key::ENTER]); + + $this->assertNull($console->resolveBlocked($blocked, 'Cancelled. No pages were published.')); + $this->assertStringContainsString('Cancelled. No pages were published.', $output->fetch()); + } + + /** + * @param array $options + * @return array{PublisherConsole, BufferedOutput} + */ + protected function makeConsole(array $options = [], bool $interactive = false): array + { + $command = $this->app->make(PublishCommand::class); + $input = new ArrayInput($options, $command->getDefinition()); + $input->setInteractive($interactive); + $output = new BufferedOutput(); + + $command->setLaravel($this->app); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + + return [new PublisherConsole($command, $input), $output]; + } + + /** + * @param array $keys + * @return array{PublisherConsole, BufferedOutput} + */ + protected function makeInteractiveConsole(array $keys): array + { + ConsoleHelper::mockWindowsOs(false); + PublisherConsolePromptReset::resetFallbacks(); + Prompt::fake($keys); + + return $this->makeConsole(interactive: true); + } + + /** @return array{source: string, target: string, absolute: string} */ + protected function blockedFile(string $target): array + { + return [ + 'source' => 'vendor/hyde/framework/resources/views/source.blade.php', + 'target' => $target, + 'absolute' => base_path($target), + ]; + } +} + +abstract class PublisherConsolePromptReset extends Prompt +{ + public static function resetFallbacks(): void + { + static::$shouldFallback = false; + } +} diff --git a/packages/framework/tests/Feature/Commands/RebuildPageCommandTest.php b/packages/framework/tests/Feature/Commands/RebuildPageCommandTest.php deleted file mode 100644 index df3e6acaa4c..00000000000 --- a/packages/framework/tests/Feature/Commands/RebuildPageCommandTest.php +++ /dev/null @@ -1,83 +0,0 @@ -file('_pages/test-page.md', 'foo'); - - $this->artisan('rebuild _pages/test-page.md') - ->expectsOutputToContain('_site/test-page.html') - ->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_site/test-page.html')); - - $this->resetSite(); - } - - public function testCommandOutputsDeprecationWarning() - { - $this->file('_pages/test-page.md', 'foo'); - - $this->artisan('rebuild _pages/test-page.md') - ->expectsOutput('The `rebuild` command is deprecated and will be removed in HydePHP v3.0.') - ->expectsOutput('If you need to build a single page programmatically, use Hyde\Framework\Actions\StaticPageBuilder::handle() instead.') - ->assertExitCode(0); - - $this->resetSite(); - } - - public function testMediaFilesCanBeTransferred() - { - $this->directory(Hyde::path('_site/media')); - $this->file('_media/test.jpg'); - - $this->artisan('rebuild _media')->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_site/media/test.jpg')); - } - - public function testValidateCatchesBadSourceDirectory() - { - $this->artisan('rebuild foo/bar') - ->expectsOutput('Path [foo/bar] is not in a valid source directory.') - ->assertExitCode(400); - } - - public function testValidateCatchesMissingFile() - { - $this->artisan('rebuild _pages/foo.md') - ->expectsOutput('File [_pages/foo.md] not found.') - ->assertExitCode(404); - } - - public function testRebuildDocumentationPage() - { - $this->file('_docs/foo.md'); - - $this->artisan('rebuild _docs/foo.md')->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_site/docs/foo.html')); - - $this->resetSite(); - } - - public function testRebuildBlogPost() - { - $this->file('_posts/foo.md'); - - $this->artisan('rebuild _posts/foo.md')->assertExitCode(0); - - $this->assertFileExists(Hyde::path('_site/posts/foo.html')); - - $this->resetSite(); - } -} diff --git a/packages/framework/tests/Feature/HydeServiceProviderTest.php b/packages/framework/tests/Feature/HydeServiceProviderTest.php index faad0cb0fdc..f571de61f47 100644 --- a/packages/framework/tests/Feature/HydeServiceProviderTest.php +++ b/packages/framework/tests/Feature/HydeServiceProviderTest.php @@ -9,6 +9,7 @@ use Hyde\Framework\Features\Navigation\DocumentationSidebar; use Illuminate\Contracts\Container\BindingResolutionException; use Hyde\Console\ConsoleServiceProvider; +use Hyde\Foundation\Providers\ConfigurationServiceProvider; use Hyde\Framework\HydeServiceProvider; use Hyde\Framework\Services\BuildTaskService; use Hyde\Foundation\HydeCoreExtension; @@ -20,6 +21,7 @@ use Hyde\Pages\MarkdownPost; use Hyde\Testing\TestCase; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\ServiceProvider; #[\PHPUnit\Framework\Attributes\CoversClass(\Hyde\Framework\HydeServiceProvider::class)] #[\PHPUnit\Framework\Attributes\CoversClass(\Hyde\Framework\Concerns\RegistersFileLocations::class)] @@ -371,4 +373,34 @@ public function testCanGetDocumentationSidebarFromContainerUsingShorthand() $this->assertSame(app('navigation.sidebar'), DocumentationSidebar::get()); } + + public function testHydeConfigPublishTagPublishesExactlyTheSixHydeOwnedConfigFiles() + { + $paths = ServiceProvider::pathsToPublish(ConfigurationServiceProvider::class, 'hyde-config'); + + $sources = array_map('basename', array_keys($paths)); + $destinations = array_map('basename', array_values($paths)); + + $expected = ['hyde.php', 'docs.php', 'markdown.php', 'view.php', 'cache.php', 'commands.php']; + + $this->assertSame($expected, $sources); + $this->assertSame($expected, $destinations); + } + + public function testHydeConfigPublishTagDoesNotPublishTorchlightConfig() + { + $paths = ServiceProvider::pathsToPublish(ConfigurationServiceProvider::class, 'hyde-config'); + + $files = array_map('basename', array_keys($paths)); + + $this->assertNotContains('torchlight.php', $files); + } + + public function testLegacyConfigPublishTagsAreRemovedAndPublishNothing() + { + // hyde-config is the only Hyde config publish tag in v3; the legacy tags publish nothing. + foreach (['configs', 'hyde-configs', 'support-configs'] as $tag) { + $this->assertSame([], ServiceProvider::pathsToPublish(ConfigurationServiceProvider::class, $tag)); + } + } } diff --git a/packages/framework/tests/Feature/PostsAuthorIntegrationTest.php b/packages/framework/tests/Feature/PostsAuthorIntegrationTest.php index 85c653095ec..be5d11955ef 100644 --- a/packages/framework/tests/Feature/PostsAuthorIntegrationTest.php +++ b/packages/framework/tests/Feature/PostsAuthorIntegrationTest.php @@ -6,7 +6,9 @@ use Hyde\Facades\Author; use Hyde\Pages\MarkdownPost; +use Hyde\Foundation\Facades\Pages; use Hyde\Framework\Actions\CreatesNewMarkdownPostFile; +use Hyde\Framework\Actions\StaticPageBuilder; use Hyde\Hyde; use Hyde\Testing\TestCase; use Illuminate\Support\Facades\Config; @@ -34,7 +36,7 @@ public function testCreatePostWithUndefinedAuthor() { $this->createPostFile('post-with-undefined-author', 'test_undefined_author'); - $this->artisan('rebuild _posts/post-with-undefined-author.md')->assertExitCode(0); + StaticPageBuilder::handle(Pages::getPage('_posts/post-with-undefined-author.md')); $this->assertFileExists(Hyde::path('_site/posts/post-with-undefined-author.html')); // Check that the author is rendered as is in the DOM @@ -55,7 +57,7 @@ public function testCreatePostWithDefinedAuthorWithName() 'named_author' => Author::create('Test Author', null), ]); - $this->artisan('rebuild _posts/post-with-defined-author-with-name.md')->assertExitCode(0); + StaticPageBuilder::handle(Pages::getPage('_posts/post-with-defined-author-with-name.md')); $this->assertFileExists(Hyde::path('_site/posts/post-with-defined-author-with-name.html')); // Check that the author is contains the set name in the DOM @@ -76,7 +78,7 @@ public function testCreatePostWithDefinedAuthorWithWebsite() 'test_author_with_website' => Author::create('Test Author', 'https://example.org'), ]); - $this->artisan('rebuild _posts/post-with-defined-author-with-name.md')->assertExitCode(0); + StaticPageBuilder::handle(Pages::getPage('_posts/post-with-defined-author-with-name.md')); $this->assertFileExists(Hyde::path('_site/posts/post-with-defined-author-with-name.html')); // Check that the author is contains the set name in the DOM @@ -111,7 +113,7 @@ public function testAllPostAuthorFieldsCanBeSetInFrontMatter() MD ); - $this->artisan('rebuild _posts/post-with-all-author-fields.md')->assertExitCode(0); + StaticPageBuilder::handle(Pages::getPage('_posts/post-with-all-author-fields.md')); $this->cleanUpWhenDone('_site/posts/post-with-all-author-fields.html'); $this->assertFileExists(Hyde::path('_site/posts/post-with-all-author-fields.html')); @@ -167,8 +169,8 @@ public function testConfiguredPostAuthorFieldsCanBeOverriddenInFrontMatter() MD ); - $this->artisan('rebuild _posts/literal.md')->assertExitCode(0); - $this->artisan('rebuild _posts/changed.md')->assertExitCode(0); + StaticPageBuilder::handle(Pages::getPage('_posts/literal.md')); + StaticPageBuilder::handle(Pages::getPage('_posts/changed.md')); $this->assertFileExists(Hyde::path('_site/posts/literal.html')); $this->assertFileExists(Hyde::path('_site/posts/changed.html')); $this->cleanUpWhenDone('_site/posts/literal.html'); diff --git a/packages/framework/tests/Feature/Views/MetadataViewTest.php b/packages/framework/tests/Feature/Views/MetadataViewTest.php index fb23472ea34..ad3bc28796a 100644 --- a/packages/framework/tests/Feature/Views/MetadataViewTest.php +++ b/packages/framework/tests/Feature/Views/MetadataViewTest.php @@ -7,6 +7,8 @@ use Hyde\Hyde; use Hyde\Testing\TestCase; use Hyde\Foundation\HydeKernel; +use Hyde\Foundation\Facades\Pages; +use Hyde\Framework\Actions\StaticPageBuilder; /** * This tests ensures all metadata is rendered correctly in the compiled pages. @@ -33,7 +35,7 @@ protected function setUp(): void protected function build(?string $page = null): void { if ($page) { - $this->artisan("rebuild $page"); + StaticPageBuilder::handle(Pages::getPage($page)); } else { $this->artisan('build'); } diff --git a/packages/framework/tests/Unit/HtmlTestingSupportMetaTest.php b/packages/framework/tests/Unit/HtmlTestingSupportMetaTest.php index bdec65b7734..e35afd8cfe9 100644 --- a/packages/framework/tests/Unit/HtmlTestingSupportMetaTest.php +++ b/packages/framework/tests/Unit/HtmlTestingSupportMetaTest.php @@ -281,7 +281,7 @@ public function testComplexTextRepresentationParsing() Leap into the future of static HTML blogs and documentation with the tools you already know and love. Made with Tailwind, Laravel, and Coffee. This is the default homepage stored as index.blade.php, however you can publish any of the built-in views using the following command: -php hyde php hyde php hyde publish:homepage +php hyde php hyde php hyde publish --page Resources for getting started Documentation Getting Started diff --git a/packages/framework/tests/Unit/InteractivePublishCommandHelperTest.php b/packages/framework/tests/Unit/InteractivePublishCommandHelperTest.php deleted file mode 100644 index 22198a4a607..00000000000 --- a/packages/framework/tests/Unit/InteractivePublishCommandHelperTest.php +++ /dev/null @@ -1,202 +0,0 @@ -filesystem = $this->mockFilesystemStrict(); - - app()->instance(Filesystem::class, $this->filesystem); - } - - protected function tearDown(): void - { - $this->verifyMockeryExpectations(); - - app()->forgetInstance(Filesystem::class); - } - - public function testGetFileChoices(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - ]); - - $this->assertSame([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'post.blade.php', - ], $helper->getFileChoices()); - } - - public function testOnlyFiltersPublishableFiles(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - ]); - - $helper->only([ - 'packages/framework/resources/views/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php', - ]); - - $this->assertSame([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'app.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'post.blade.php', - ], $helper->getFileChoices()); - } - - public function testPublishFiles(): void - { - $this->filesystem->shouldReceive('dirname')->times(3)->andReturn(Hyde::path('resources/views/vendor/hyde/layouts')); - $this->filesystem->shouldReceive('ensureDirectoryExists')->times(3); - $this->filesystem->shouldReceive('copy')->times(3); - - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - ]); - - $helper->publishFiles(); - - $this->filesystem->shouldHaveReceived('dirname')->times(3); - $this->filesystem->shouldHaveReceived('ensureDirectoryExists')->with(Hyde::path('resources/views/vendor/hyde/layouts'), 0755, true)->times(3); - - $this->filesystem->shouldHaveReceived('copy')->with( - Hyde::path('packages/framework/resources/views/layouts/app.blade.php'), - Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php') - )->once(); - - $this->filesystem->shouldHaveReceived('copy')->with( - Hyde::path('packages/framework/resources/views/layouts/page.blade.php'), - Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php') - )->once(); - - $this->filesystem->shouldHaveReceived('copy')->with( - Hyde::path('packages/framework/resources/views/layouts/post.blade.php'), - Hyde::path('resources/views/vendor/hyde/layouts/post.blade.php') - )->once(); - } - - public function testFormatOutputForSingleFile(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - ]); - - $this->assertSame( - 'Published selected file to [resources/views/vendor/hyde/layouts/app.blade.php]', - $helper->formatOutput('layouts') - ); - } - - public function testFormatOutputForMultipleFiles(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - ]); - - $this->assertSame( - 'Published all 2 files to [resources/views/vendor/hyde/layouts]', - $helper->formatOutput('all') - ); - } - - public function testFormatOutputForSingleChosenFile(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - ]); - - $helper->only([ - 'packages/framework/resources/views/layouts/app.blade.php', - ]); - - $this->assertSame( - 'Published selected file to [resources/views/vendor/hyde/layouts/app.blade.php]', - $helper->formatOutput('layouts') - ); - } - - public function testFormatOutputForMultipleChosenFiles(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - ]); - - $helper->only([ - 'packages/framework/resources/views/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php', - ]); - - $this->assertSame( - 'Published selected [layout] files to [resources/views/vendor/hyde/layouts]', - $helper->formatOutput('layouts') - ); - } - - public function testGetBaseDirectoryWithOneSet(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - ]); - - $this->assertSame( - 'resources/views/vendor/hyde/layouts', - $helper->getBaseDirectory() - ); - } - - public function testGetBaseDirectoryWithMultipleSets(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', - 'packages/framework/resources/views/components/page.blade.php' => 'resources/views/vendor/hyde/components/page.blade.php', - ]); - - $this->assertSame( - 'resources/views/vendor/hyde', - $helper->getBaseDirectory() - ); - } - - public function testGetBaseDirectoryWithSinglePath(): void - { - $helper = new InteractivePublishCommandHelper([ - 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', - ]); - - $this->assertSame( - 'resources/views/vendor/hyde/layouts/app.blade.php', - $helper->getBaseDirectory() - ); - } -} diff --git a/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php b/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php index 585ffebfb06..c1dc7be7599 100644 --- a/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php +++ b/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php @@ -37,24 +37,6 @@ public function testMediaOutputDirectoryCanBeChangedForSiteBuilds() $this->resetSite(); } - public function testMediaOutputDirectoryCanBeChangedForSiteRebuilds() - { - Filesystem::deleteDirectory('_site'); - - $this->directory('_assets'); - $this->file('_assets/app.css'); - - Hyde::setMediaDirectory('_assets'); - - $this->artisan('rebuild _assets'); - - $this->assertDirectoryDoesNotExist(Hyde::path('_site/media')); - $this->assertDirectoryExists(Hyde::path('_site/assets')); - $this->assertFileExists(Hyde::path('_site/assets/app.css')); - - $this->resetSite(); - } - public function testCompiledPagesHaveLinksToTheRightMediaFileLocation() { Filesystem::moveDirectory('_media', '_assets'); diff --git a/packages/framework/tests/Unit/OverwritePolicyTest.php b/packages/framework/tests/Unit/OverwritePolicyTest.php new file mode 100644 index 00000000000..4bd0ae43d12 --- /dev/null +++ b/packages/framework/tests/Unit/OverwritePolicyTest.php @@ -0,0 +1,111 @@ +source = 'overwrite-policy-source-'.uniqid().'.tmp'; + $this->destination = 'overwrite-policy-destination-'.uniqid().'.tmp'; + } + + protected function tearDown(): void + { + foreach ([$this->source, $this->destination] as $path) { + if (is_file(Hyde::path($path))) { + unlink(Hyde::path($path)); + } + + if (is_dir(Hyde::path($path))) { + rmdir(Hyde::path($path)); + } + } + } + + public function testDecidesToCopyWhenDestinationIsMissing() + { + $this->putSource('Hello world'); + + $this->assertSame(OverwriteAction::Copy, OverwritePolicy::decide($this->source, $this->destination)); + } + + public function testDecidesToSkipWhenDestinationIsIdenticalToSource() + { + $this->putSource('Hello world'); + $this->putDestination('Hello world'); + + $this->assertSame(OverwriteAction::Skip, OverwritePolicy::decide($this->source, $this->destination)); + } + + public function testDecidesToBlockWhenDestinationDiffersFromSource() + { + $this->putSource('Hello world'); + $this->putDestination('Hello world, but modified by the user'); + + $this->assertSame(OverwriteAction::Blocked, OverwritePolicy::decide($this->source, $this->destination)); + } + + public function testDecidesToSkipWhenFilesDifferOnlyByLineEndings() + { + $this->putSource("Hello\nworld\n"); + $this->putDestination("Hello\r\nworld\r\n"); + + $this->assertSame(OverwriteAction::Skip, OverwritePolicy::decide($this->source, $this->destination)); + } + + public function testReturnsErrorWhenSourceFileIsMissing() + { + $this->putDestination('Existing destination'); + + $this->assertSame(OverwriteAction::Error, OverwritePolicy::decide($this->source, $this->destination)); + } + + public function testReturnsErrorWhenSourceIsADirectory() + { + mkdir(Hyde::path($this->source)); + + $this->assertSame(OverwriteAction::Error, OverwritePolicy::decide($this->source, $this->destination)); + } + + public function testReturnsErrorWhenDestinationIsADirectory() + { + $this->putSource('Hello world'); + mkdir(Hyde::path($this->destination)); + + $this->assertSame(OverwriteAction::Error, OverwritePolicy::decide($this->source, $this->destination)); + } + + protected function putSource(string $contents): void + { + file_put_contents(Hyde::path($this->source), $contents); + } + + protected function putDestination(string $contents): void + { + file_put_contents(Hyde::path($this->destination), $contents); + } +} diff --git a/packages/framework/tests/Unit/PublishablePageTest.php b/packages/framework/tests/Unit/PublishablePageTest.php new file mode 100644 index 00000000000..1b707eb912b --- /dev/null +++ b/packages/framework/tests/Unit/PublishablePageTest.php @@ -0,0 +1,75 @@ + 'Use as your site homepage'], + allowCustomTarget: false, + ); + + $this->assertSame('posts', $page->key); + $this->assertSame('Posts feed', $page->label); + $this->assertSame('A feed of your latest posts.', $page->description); + $this->assertSame('resources/views/homepages/post-feed.blade.php', $page->source); + $this->assertSame('_pages/posts.blade.php', $page->defaultTarget); + $this->assertSame(['_pages/index.blade.php' => 'Use as your site homepage'], $page->alternativeTargets); + $this->assertFalse($page->allowCustomTarget); + $this->assertSame(BladePage::class, $page->pageClass); + } + + public function testOptionalPropertiesDefaultToNoAlternativesCustomTargetAllowedAndBladePageClass() + { + $page = new PublishablePage( + key: 'welcome', + label: 'Welcome page', + description: 'The default welcome page.', + source: 'resources/views/homepages/welcome.blade.php', + defaultTarget: '_pages/index.blade.php', + ); + + $this->assertSame([], $page->alternativeTargets); + $this->assertTrue($page->allowCustomTarget); + $this->assertSame(BladePage::class, $page->pageClass); + } + + public function testDefaultTargetMayBeNullForPagesWithoutADefaultDestination() + { + $page = new PublishablePage( + key: 'blank', + label: 'Blank page', + description: 'A blank Blade template with just the base layout.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: null, + ); + + $this->assertNull($page->defaultTarget); + } + + public function testValueObjectIsImmutable() + { + $reflection = new ReflectionClass(PublishablePage::class); + + $this->assertTrue($reflection->isFinal()); + + foreach ($reflection->getProperties() as $property) { + $this->assertTrue($property->isReadOnly(), "Property {$property->getName()} should be readonly."); + } + } +} diff --git a/packages/framework/tests/Unit/PublishablePagesTest.php b/packages/framework/tests/Unit/PublishablePagesTest.php new file mode 100644 index 00000000000..98125e1d916 --- /dev/null +++ b/packages/framework/tests/Unit/PublishablePagesTest.php @@ -0,0 +1,141 @@ +key property since PHP coerces the numeric '404' array key to an integer. + $this->assertSame(['welcome', 'posts', 'blank', '404'], $this->catalogKeys()); + } + + public function testAllReturnsPublishablePageInstancesRetrievableByTheirKey() + { + foreach (PublishablePages::all() as $page) { + $this->assertInstanceOf(PublishablePage::class, $page); + $this->assertSame($page, PublishablePages::get($page->key)); + } + } + + public function testDefaultCatalogTargets() + { + $pages = PublishablePages::all(); + + $this->assertSame('_pages/index.blade.php', $pages['welcome']->defaultTarget); + $this->assertSame('_pages/posts.blade.php', $pages['posts']->defaultTarget); + $this->assertNull($pages['blank']->defaultTarget, 'blank has no default target; its destination is always prompted for or set via --to.'); + $this->assertSame('_pages/404.blade.php', $pages['404']->defaultTarget); + } + + public function testDefaultCatalogTargetsFollowTheConfiguredBladePageSourceDirectory() + { + BladePage::setSourceDirectory('content/pages'); + PublishablePages::clear(); + + $pages = PublishablePages::all(); + + $this->assertSame('content/pages/index.blade.php', $pages['welcome']->defaultTarget); + $this->assertSame('content/pages/posts.blade.php', $pages['posts']->defaultTarget); + $this->assertSame('content/pages/404.blade.php', $pages['404']->defaultTarget); + $this->assertSame(['content/pages/index.blade.php' => 'Use as your site homepage'], $pages['posts']->alternativeTargets); + } + + public function testDefaultCatalogSources() + { + $pages = PublishablePages::all(); + + $this->assertSame('resources/views/homepages/welcome.blade.php', $pages['welcome']->source); + $this->assertSame('resources/views/homepages/post-feed.blade.php', $pages['posts']->source); + $this->assertSame('resources/views/homepages/blank.blade.php', $pages['blank']->source); + $this->assertSame('resources/views/pages/404.blade.php', $pages['404']->source); + } + + public function testOnlyPostsDeclaresAnAlternativeHomepageTarget() + { + $pages = PublishablePages::all(); + + $this->assertSame(['_pages/index.blade.php' => 'Use as your site homepage'], $pages['posts']->alternativeTargets); + $this->assertSame([], $pages['welcome']->alternativeTargets); + $this->assertSame([], $pages['blank']->alternativeTargets); + $this->assertSame([], $pages['404']->alternativeTargets); + } + + public function testOnlyThe404PageForbidsCustomTargets() + { + $pages = PublishablePages::all(); + + $this->assertTrue($pages['welcome']->allowCustomTarget); + $this->assertTrue($pages['posts']->allowCustomTarget); + $this->assertTrue($pages['blank']->allowCustomTarget); + $this->assertFalse($pages['404']->allowCustomTarget); + } + + public function testGetReturnsPageByKey() + { + $this->assertSame('welcome', PublishablePages::get('welcome')->key); + } + + public function testGetReturnsNullForUnknownKey() + { + $this->assertNull(PublishablePages::get('does-not-exist')); + } + + public function testRegisterAddsANewPage() + { + PublishablePages::register(new PublishablePage( + key: 'changelog', + label: 'Changelog page', + description: 'A changelog for your site.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: '_pages/changelog.blade.php', + )); + + $this->assertArrayHasKey('changelog', PublishablePages::all()); + $this->assertSame('changelog', PublishablePages::get('changelog')->key); + $this->assertSame(['welcome', 'posts', 'blank', '404', 'changelog'], $this->catalogKeys()); + } + + public function testRegisterOverridesAPageSharingItsKey() + { + PublishablePages::register(new PublishablePage( + key: 'welcome', + label: 'Custom welcome page', + description: 'An overridden welcome page.', + source: 'resources/views/homepages/blank.blade.php', + defaultTarget: '_pages/index.blade.php', + )); + + $this->assertSame('Custom welcome page', PublishablePages::get('welcome')->label); + $this->assertCount(4, PublishablePages::all()); + } + + /** @return array The page keys read from the ->key property (immune to PHP numeric-key coercion). */ + protected function catalogKeys(): array + { + return array_values(array_map(fn (PublishablePage $page): string => $page->key, PublishablePages::all())); + } +} diff --git a/packages/framework/tests/Unit/ViewDiffServiceTest.php b/packages/framework/tests/Unit/ViewDiffServiceTest.php deleted file mode 100644 index d8f06f2fa61..00000000000 --- a/packages/framework/tests/Unit/ViewDiffServiceTest.php +++ /dev/null @@ -1,48 +0,0 @@ -assertIsArray($fileCache); - $this->assertArrayHasKey('resources/views/layouts/app.blade.php', $fileCache); - $this->assertArrayHasKey('unixsum', $fileCache['resources/views/layouts/app.blade.php']); - $this->assertSame(32, strlen($fileCache['resources/views/layouts/app.blade.php']['unixsum'])); - } - - public function testGetChecksums() - { - $checksums = ViewDiffService::getChecksums(); - - $this->assertIsArray($checksums); - $this->assertSame(32, strlen($checksums[0])); - } - - public function testChecksumMatchesAny() - { - $this->assertTrue(ViewDiffService::checksumMatchesAny( - unixsum_file(Hyde::vendorPath('resources/views/layouts/app.blade.php')) - )); - } - - public function testChecksumMatchesAnyFalse() - { - $this->assertFalse(ViewDiffService::checksumMatchesAny(unixsum('foo'))); - } -} diff --git a/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php b/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php index e1aa632ac05..f8a5c578046 100644 --- a/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php +++ b/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php @@ -6,6 +6,8 @@ use Illuminate\Support\Str; use Hyde\Facades\Filesystem; +use Hyde\Foundation\Facades\Pages; +use Hyde\Framework\Actions\StaticPageBuilder; use Hyde\Hyde; use Hyde\Testing\TestCase; use Hyde\Pages\MarkdownPage; @@ -137,7 +139,7 @@ public function testNavigationMenuLabelCanBeChangedInFrontMatter() '); Hyde::boot(); - $this->artisan('rebuild _pages/foo.md'); + StaticPageBuilder::handle(Pages::getPage('_pages/foo.md')); $this->assertStringContainsString('My custom label', file_get_contents(Hyde::path('_site/foo.html'))); Filesystem::unlink('_site/foo.html'); } @@ -151,7 +153,7 @@ public function testNavigationMenuLabelCanBeChangedInBladeMatter() ); Hyde::boot(); - $this->artisan('rebuild _pages/foo.blade.php'); + StaticPageBuilder::handle(Pages::getPage('_pages/foo.blade.php')); $this->assertStringContainsString('My custom label', file_get_contents(Hyde::path('_site/foo.html'))); Filesystem::unlink('_site/foo.html'); } diff --git a/packages/hyde/package.json b/packages/hyde/package.json index a856c2d1710..865eda6f554 100644 --- a/packages/hyde/package.json +++ b/packages/hyde/package.json @@ -13,6 +13,6 @@ "hydefront": "^4.0.0", "postcss": "^8.5.8", "tailwindcss": "^4.1.18", - "vite": "^7.3.2" + "vite": "^8.0.0" } } diff --git a/packages/realtime-compiler/src/Http/DashboardController.php b/packages/realtime-compiler/src/Http/DashboardController.php index fdd3620848b..cd3ac35a9cd 100644 --- a/packages/realtime-compiler/src/Http/DashboardController.php +++ b/packages/realtime-compiler/src/Http/DashboardController.php @@ -238,13 +238,6 @@ public function getTip(): HtmlString public static function enabled(): bool { - /** @deprecated Previously, the setting was hyde.server.dashboard, so for backwards compatability we need this */ - if (is_bool($oldConfig = config('hyde.server.dashboard'))) { - trigger_error('Using `hyde.server.dashboard` as boolean is deprecated. Please use `hyde.server.dashboard.enabled` instead.', E_USER_DEPRECATED); - - return $oldConfig; - } - return config('hyde.server.dashboard.enabled', true); } diff --git a/packages/vite-plugin/dist/index.js b/packages/vite-plugin/dist/index.js index c421c4e59f9..3b9b38223c4 100644 --- a/packages/vite-plugin/dist/index.js +++ b/packages/vite-plugin/dist/index.js @@ -1 +1 @@ -import fs from"fs";import path from"path";import{fileURLToPath}from"url";const HOT_FILE_PATH="app/storage/framework/runtime/vite.hot";function resolveResource(e){try{const s=fileURLToPath(import.meta.url),t=path.dirname(s);return path.resolve(t,"../resources",e)}catch(s){return path.resolve(__dirname,"../resources",e)}}function fileExists(e){try{return fs.statSync(e).isFile()}catch{return!1}}function hasJavaScriptContent(e){try{if(!fs.existsSync(e))return!1;return fs.readFileSync(e,"utf-8").replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,"").trim().length>0}catch(e){return!1}}export default function hydePlugin(e={}){const{input:s=["resources/assets/app.css","resources/assets/app.js"],watch:t=["_pages","_posts","_docs"],refresh:r=!0}=e;let a,n;return{name:"hyde-vite-plugin",config(e,{command:t}){if("build"===t){const e={};for(const t of s){const s=path.resolve(process.cwd(),t);if(t.endsWith("app.js"))hasJavaScriptContent(s)&&fileExists(s)&&(e.js=s);else if(t.endsWith("app.css")&&fileExists(s))e.css=s;else if(fileExists(s)){e[path.basename(t,path.extname(t))]=s}}return{build:{outDir:"_media",emptyOutDir:!1,rollupOptions:{input:e,output:{entryFileNames:e=>"js"===e.name?"app.js":"[name].js",chunkFileNames:"[name].js",assetFileNames:e=>e.name&&e.name.endsWith(".css")?"app.css":"[name].[ext]"}}}}}},configResolved(e){a=e,n=path.resolve(process.cwd(),HOT_FILE_PATH)},configureServer(e){fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,""),["SIGINT","SIGTERM"].forEach((e=>{process.on(e,(()=>{try{fs.rmSync(n)}catch(e){}process.exit()}))})),e.middlewares.use(((e,s,t)=>{if("/"===e.url)try{const e=resolveResource("index.html"),t=fs.readFileSync(e,"utf-8");s.end(t)}catch(e){t()}else t()})),r&&t.forEach((s=>{const t=path.resolve(process.cwd(),s);fs.existsSync(t)&&(e.watcher.add(path.join(t,"**")),e.watcher.add(path.join(t,"**/**")))}))}}} \ No newline at end of file +import fs from"fs";import path from"path";import{fileURLToPath}from"url";const HOT_FILE_PATH="app/storage/framework/runtime/vite.hot";function resolveResource(e){try{const s=fileURLToPath(import.meta.url),t=path.dirname(s);return path.resolve(t,"../resources",e)}catch(s){return path.resolve(__dirname,"../resources",e)}}function fileExists(e){try{return fs.statSync(e).isFile()}catch{return!1}}function hasJavaScriptContent(e){try{if(!fs.existsSync(e))return!1;return fs.readFileSync(e,"utf-8").replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,"").trim().length>0}catch(e){return!1}}export default function hydePlugin(e={}){const{input:s=["resources/assets/app.css","resources/assets/app.js"],watch:t=["_pages","_posts","_docs"],refresh:r=!0}=e;let a,n;return{name:"hyde-vite-plugin",config(e,{command:t}){if("build"===t){const e={};for(const t of s){const s=path.resolve(process.cwd(),t);if(t.endsWith("app.js"))hasJavaScriptContent(s)&&fileExists(s)&&(e.js=s);else if(t.endsWith("app.css")&&fileExists(s))e.css=s;else if(fileExists(s)){e[path.basename(t,path.extname(t))]=s}}return{build:{outDir:"_media",emptyOutDir:!1,rolldownOptions:{input:e,output:{entryFileNames:e=>"js"===e.name?"app.js":"[name].js",chunkFileNames:"[name].js",assetFileNames:e=>e.name&&e.name.endsWith(".css")?"app.css":"[name].[ext]"}}}}}},configResolved(e){a=e,n=path.resolve(process.cwd(),HOT_FILE_PATH)},configureServer(e){fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,""),["SIGINT","SIGTERM"].forEach((e=>{process.on(e,(()=>{try{fs.rmSync(n)}catch(e){}process.exit()}))})),e.middlewares.use(((e,s,t)=>{if("/"===e.url)try{const e=resolveResource("index.html"),t=fs.readFileSync(e,"utf-8");s.end(t)}catch(e){t()}else t()})),r&&t.forEach((s=>{const t=path.resolve(process.cwd(),s);fs.existsSync(t)&&(e.watcher.add(path.join(t,"**")),e.watcher.add(path.join(t,"**/**")))}))}}} \ No newline at end of file diff --git a/packages/vite-plugin/package-lock.json b/packages/vite-plugin/package-lock.json index 13bd9e29409..fdc679e518d 100644 --- a/packages/vite-plugin/package-lock.json +++ b/packages/vite-plugin/package-lock.json @@ -12,452 +12,44 @@ "@types/node": "^20.0.0", "terser": "^5.29.2", "typescript": "^5.0.0", - "vite": "^7.3.2" + "vite": "^8.0.0" }, "peerDependencies": { - "vite": ">=6.3.5 <8.0.0" + "vite": ">=6.3.5 <9.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -524,66 +116,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -591,55 +156,16 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" + "android" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -647,167 +173,187 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" + "darwin" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" + "darwin" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ - "s390x" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ - "x64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -816,54 +362,51 @@ "optional": true, "os": [ "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ - "ia32" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -872,15 +415,29 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/node": { "version": "20.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", @@ -918,46 +475,14 @@ "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "license": "Apache-2.0", "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "node": ">=8" } }, "node_modules/fdir": { @@ -993,10 +518,283 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -1020,9 +818,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -1033,9 +831,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -1053,7 +851,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1061,49 +859,38 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/source-map": { @@ -1157,14 +944,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -1173,6 +960,14 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -1194,18 +989,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -1221,9 +1015,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -1236,13 +1031,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 0c3310666fd..7fec29cc275 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -27,12 +27,12 @@ }, "homepage": "https://github.com/hydephp/vite-plugin", "peerDependencies": { - "vite": ">=6.3.5 <8.0.0" + "vite": ">=6.3.5 <9.0.0" }, "devDependencies": { "@types/node": "^20.0.0", "terser": "^5.29.2", "typescript": "^5.0.0", - "vite": "^7.3.2" + "vite": "^8.0.0" } -} +} \ No newline at end of file diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 86621127bba..62c30f2d193 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -91,7 +91,7 @@ export default function hydePlugin(options: HydePluginOptions = {}): Plugin { if (command === 'build') { // Process input files - use named keys to prevent filename collisions const resolvedInput: Record = {}; - + for (const entry of input) { const resolvedPath = path.resolve(process.cwd(), entry); @@ -112,10 +112,10 @@ export default function hydePlugin(options: HydePluginOptions = {}): Plugin { build: { outDir: '_media', emptyOutDir: false, - rollupOptions: { + rolldownOptions: { input: resolvedInput, output: { - entryFileNames: (chunkInfo) => { + entryFileNames: (chunkInfo: any) => { // Use app.js for the JS entry point if (chunkInfo.name === 'js') { return 'app.js'; @@ -123,7 +123,7 @@ export default function hydePlugin(options: HydePluginOptions = {}): Plugin { return '[name].js'; }, chunkFileNames: '[name].js', - assetFileNames: (assetInfo) => { + assetFileNames: (assetInfo: any) => { // Use app.css for CSS assets if (assetInfo.name && assetInfo.name.endsWith('.css')) { return 'app.css'; @@ -186,4 +186,4 @@ export default function hydePlugin(options: HydePluginOptions = {}): Plugin { } } }; -} +} \ No newline at end of file diff --git a/packages/vite-plugin/tsconfig.json b/packages/vite-plugin/tsconfig.json index 975c01ca556..89b8415d872 100644 --- a/packages/vite-plugin/tsconfig.json +++ b/packages/vite-plugin/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "ES2020", - "moduleResolution": "node", + "moduleResolution": "bundler", "esModuleInterop": true, "declaration": true, "outDir": "./dist",