From 2255b4b714e5886528d59bdf4d475c1080a2736f Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Thu, 15 Oct 2026 11:29:27 +0200 Subject: [PATCH] feat: require the pollora/assets package --- CHANGELOG.md | 7 + composer.json | 1 + .../Application/Services/AssetManager.php | 154 ----- .../Services/AssetRegistrationService.php | 37 - .../Services/AssetRetrievalService.php | 45 -- .../Application/Services/MissingBuilds.php | 51 -- .../Domain/Contracts/AssetFileInterface.php | 34 - .../Contracts/AssetRepositoryInterface.php | 39 -- src/Asset/Domain/Contracts/PublicPath.php | 16 - src/Asset/Domain/Contracts/RuntimeContext.php | 16 - .../Domain/Contracts/ViteManagerFactory.php | 21 - .../Domain/Contracts/ViteManagerInterface.php | 55 -- .../Domain/Exceptions/AssetException.php | 16 - src/Asset/Domain/Models/Asset.php | 47 -- src/Asset/Domain/Models/AssetFile.php | 76 -- src/Asset/Domain/Models/ViteManager.php | 64 -- .../Domain/Services/AssetContainerManager.php | 55 -- .../Providers/AssetServiceProvider.php | 2 +- .../Repositories/AssetContainer.php | 102 --- .../Repositories/InMemoryAssetRepository.php | 56 -- .../Infrastructure/Services/AssetEnqueuer.php | 654 ------------------ .../Infrastructure/Services/AssetFile.php | 47 -- src/Asset/UI/Http/MissingBuildNotice.php | 72 -- tests/Feature/Asset/AssetEnqueuerTest.php | 415 ----------- .../Asset/AssetManagerContainersTest.php | 23 - tests/Feature/Asset/ViteManagerTest.php | 5 +- tests/Unit/Asset/AssetDomainTest.php | 55 -- 27 files changed, 12 insertions(+), 2153 deletions(-) delete mode 100755 src/Asset/Application/Services/AssetManager.php delete mode 100755 src/Asset/Application/Services/AssetRegistrationService.php delete mode 100755 src/Asset/Application/Services/AssetRetrievalService.php delete mode 100644 src/Asset/Application/Services/MissingBuilds.php delete mode 100755 src/Asset/Domain/Contracts/AssetFileInterface.php delete mode 100755 src/Asset/Domain/Contracts/AssetRepositoryInterface.php delete mode 100644 src/Asset/Domain/Contracts/PublicPath.php delete mode 100644 src/Asset/Domain/Contracts/RuntimeContext.php delete mode 100644 src/Asset/Domain/Contracts/ViteManagerFactory.php delete mode 100755 src/Asset/Domain/Contracts/ViteManagerInterface.php delete mode 100755 src/Asset/Domain/Exceptions/AssetException.php delete mode 100755 src/Asset/Domain/Models/Asset.php delete mode 100755 src/Asset/Domain/Models/AssetFile.php delete mode 100755 src/Asset/Domain/Models/ViteManager.php delete mode 100755 src/Asset/Domain/Services/AssetContainerManager.php delete mode 100755 src/Asset/Infrastructure/Repositories/AssetContainer.php delete mode 100755 src/Asset/Infrastructure/Repositories/InMemoryAssetRepository.php delete mode 100755 src/Asset/Infrastructure/Services/AssetEnqueuer.php delete mode 100755 src/Asset/Infrastructure/Services/AssetFile.php delete mode 100644 src/Asset/UI/Http/MissingBuildNotice.php delete mode 100644 tests/Feature/Asset/AssetEnqueuerTest.php delete mode 100644 tests/Feature/Asset/AssetManagerContainersTest.php delete mode 100644 tests/Unit/Asset/AssetDomainTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8c0ad5..1faedd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Pollora/framework/compare/v13.36.0...develop) +### Added +- The asset classes live in their own package, [`pollora/assets`](https://github.com/Pollora/assets), which the framework requires: `AssetManager`, `AssetContainer`, `AssetEnqueuer` (`Asset::add()->container()->useVite()`), `AssetFile`, the record and the wp-admin notice of a missing build. Class names do not change. The package also resolves Vite builds without Laravel, for a classic theme, a plugin or a Bedrock site. The framework keeps what needs Laravel: the service provider, the `Asset` facade, `ViteManager` (Laravel's Vite, where `ASSET_URL`, a CDN and the application URL keep applying), `RootAssetManager` and the Laravel implementations of the package's three contracts (`ViteManagerFactory`, `RuntimeContext`, `PublicPath`) + +### Changed +- The asset classes that left for the package no longer reach for Laravel: `AssetEnqueuer` receives the hook services, the Vite factory, the runtime and a PSR-3 logger instead of resolving them from the container, `AssetFile` is resolved through a resolver the provider sets (`AssetFile::resolveUsing()`), and `AssetManager`, `MissingBuilds` and `MissingBuildNotice` take their public directory, logger and project root as arguments. The rendered HTML of a Pollora site is unchanged, in build and while Vite runs hot +- An `AssetEnqueuer` destroyed after its application, as in a test, no longer throws while it reports a failure of its deferred enqueue + ## [v13.36.0](https://github.com/Pollora/framework/compare/v13.35.6...v13.36.0) - 2026-10-14 ### Added diff --git a/composer.json b/composer.json index 1ab45cb6..b80baa1f 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "illuminate/console": "^13.36", "pollora/abilities": "^1.0", "pollora/ajax": "^1.1", + "pollora/assets": "^0.1", "pollora/attributes": "^0.1 || ^1.0", "pollora/discovery": "^0.1 || ^1.0", "pollora/entity": "^1.2", diff --git a/src/Asset/Application/Services/AssetManager.php b/src/Asset/Application/Services/AssetManager.php deleted file mode 100755 index 5d7bef2d..00000000 --- a/src/Asset/Application/Services/AssetManager.php +++ /dev/null @@ -1,154 +0,0 @@ - - */ - protected array $containers = []; - - /** - * The default asset container name. - */ - protected ?string $defaultContainer = null; - - /** - * Initializes the asset manager with the retrieval service. - * - * @param AssetRetrievalService $retrievalService Service for retrieving assets - * @param PublicPath|null $publicPath Where the hot file of a container without one lives - * @param (Closure(): AssetEnqueuer)|null $enqueuers Builds the enqueuer of an asset - */ - public function __construct( - private readonly AssetRetrievalService $retrievalService, - private readonly ?PublicPath $publicPath = null, - private readonly ?Closure $enqueuers = null, - ) {} - - /** - * Factory method: returns an AssetEnqueuer (builder) for fluent asset management and enqueueing. - * - * @param string $handle Asset handle - * @param string $file Asset file path - * @return AssetEnqueuer Asset enqueuer instance - */ - public function add(string $handle, string $file): AssetEnqueuer - { - if (! $this->enqueuers instanceof Closure) { - throw new AssetException('No asset enqueuer is set: the asset manager was built without one.'); - } - - return ($this->enqueuers)() - ->handle($handle) - ->path($file); - } - - /** - * Retrieves an asset by name. - * - * @param string $name Asset name - * @return Asset|null Asset instance or null if not found - */ - public function get(string $name): ?Asset - { - return $this->retrievalService->get($name); - } - - /** - * Retrieves all registered assets. - * - * @return array List of asset instances - */ - public function all(): array - { - return $this->retrievalService->all(); - } - - /** - * Adds a new asset container. - * - * @param string $name Name of the container - * @param array $config Configuration for the container - */ - public function addContainer(string $name, array $config): void - { - if (! isset($config['hot_file']) && $this->publicPath instanceof PublicPath) { - $config['hot_file'] = $this->publicPath->path($name.'.hot'); - } - - $this->containers[$name] = new AssetContainer($name, $config); - } - - /** - * Retrieves an asset container by name. - * - * @param string $name Name of the container - * @return AssetContainer|null The asset container instance or null if not found - */ - public function getContainer(string $name): ?AssetContainer - { - return $this->containers[$name] ?? null; - } - - /** - * Every asset container, keyed by name: the theme's, each plugin's and - * module's, for tools that list where a page's assets come from. - * - * @return array - */ - public function containers(): array - { - return $this->containers; - } - - /** - * Sets the default asset container. - * - * @param string $name Name of the container to set as default - */ - public function setDefaultContainer(string $name): void - { - $this->defaultContainer = $name; - } - - /** - * Gets the default asset container instance. - * - * @return AssetContainer|null The default asset container instance or null if not set - */ - public function getDefaultContainer(): ?AssetContainer - { - return in_array($this->defaultContainer, [null, '', '0'], true) ? null : $this->getContainer($this->defaultContainer); - } - - /** - * Infrastructure factory for asset file URL resolution (Vite, containers, etc.). - * - * @param string $file Asset file path - * @return AssetFile Asset file instance for URL resolution - */ - public function url(string $file): AssetFile - { - return new AssetFile($file); - } -} diff --git a/src/Asset/Application/Services/AssetRegistrationService.php b/src/Asset/Application/Services/AssetRegistrationService.php deleted file mode 100755 index 71a8d8f8..00000000 --- a/src/Asset/Application/Services/AssetRegistrationService.php +++ /dev/null @@ -1,37 +0,0 @@ -manager->registerAsset($asset); - } -} diff --git a/src/Asset/Application/Services/AssetRetrievalService.php b/src/Asset/Application/Services/AssetRetrievalService.php deleted file mode 100755 index 1e72aecf..00000000 --- a/src/Asset/Application/Services/AssetRetrievalService.php +++ /dev/null @@ -1,45 +0,0 @@ -manager->getAsset($name); - } - - /** - * Retrieves all registered assets. - * - * @return array List of all asset instances - */ - public function all(): array - { - return $this->manager->getAllAssets(); - } -} diff --git a/src/Asset/Application/Services/MissingBuilds.php b/src/Asset/Application/Services/MissingBuilds.php deleted file mode 100644 index b38ca17a..00000000 --- a/src/Asset/Application/Services/MissingBuilds.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - private array $missing = []; - - public function record(string $container, string $manifest): void - { - if (isset($this->missing[$container])) { - return; - } - - $this->missing[$container] = $manifest; - - ($this->logger ?? new NullLogger)->warning(sprintf('Pollora: the "%s" assets are not built, no Vite manifest at %s. Its scripts and styles are left out until you run npm run build.', $container, $manifest), [ - 'container' => $container, - 'manifest' => $manifest, - ]); - } - - /** - * @return array Manifest path by container name - */ - public function all(): array - { - return $this->missing; - } -} diff --git a/src/Asset/Domain/Contracts/AssetFileInterface.php b/src/Asset/Domain/Contracts/AssetFileInterface.php deleted file mode 100755 index 6755eb23..00000000 --- a/src/Asset/Domain/Contracts/AssetFileInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -name; - } - - /** - * Gets the file path or URL to the asset. - */ - public function getPath(): string - { - return $this->path; - } - - /** - * Gets the asset's additional attributes. - */ - public function getAttributes(): array - { - return $this->attributes; - } -} diff --git a/src/Asset/Domain/Models/AssetFile.php b/src/Asset/Domain/Models/AssetFile.php deleted file mode 100755 index 57c2ca5f..00000000 --- a/src/Asset/Domain/Models/AssetFile.php +++ /dev/null @@ -1,76 +0,0 @@ -filename; - } - - /** - * Gets the asset container identifier. - * - * @return string The container identifier - */ - public function getAssetContainer(): string - { - return $this->assetContainer; - } - - /** - * Sets the asset container to use. - * - * @param string $container The container identifier - */ - public function from(string $container): static - { - $this->assetContainer = $container; - - return $this; - } - - /** - * Converts the asset file to its string representation (filename). - * - * @return string The file name or path - */ - public function __toString(): string - { - return $this->filename; - } -} diff --git a/src/Asset/Domain/Models/ViteManager.php b/src/Asset/Domain/Models/ViteManager.php deleted file mode 100755 index 312852c9..00000000 --- a/src/Asset/Domain/Models/ViteManager.php +++ /dev/null @@ -1,64 +0,0 @@ -repository->save($asset); - } - - /** - * Retrieves a single asset by name. - * - * @param string $name Asset name/handle - * @return Asset|null The asset instance, or null if not found - */ - public function getAsset(string $name): ?Asset - { - return $this->repository->findByName($name); - } - - /** - * Retrieves all registered assets. - * - * @return array List of all asset instances - */ - public function getAllAssets(): array - { - return $this->repository->all(); - } -} diff --git a/src/Asset/Infrastructure/Providers/AssetServiceProvider.php b/src/Asset/Infrastructure/Providers/AssetServiceProvider.php index ce5f1b69..26f05c1e 100755 --- a/src/Asset/Infrastructure/Providers/AssetServiceProvider.php +++ b/src/Asset/Infrastructure/Providers/AssetServiceProvider.php @@ -65,7 +65,7 @@ public function register(): void $app->make(LoggerInterface::class), )); AssetFile::resolveUsing(Closure::fromCallable(new LaravelAssetFileResolver($this->app))); - $this->app->bind(MissingBuildNotice::class, fn (Application $app): MissingBuildNotice => new MissingBuildNotice($app->make(MissingBuilds::class), base_path())); + $this->app->bind(MissingBuildNotice::class, fn (Application $app): MissingBuildNotice => new MissingBuildNotice($app->make(MissingBuilds::class), base_path(), 'php artisan pollora:doctor')); $this->app->singleton(MissingBuilds::class); $this->app->singleton(RootAssetManager::class, fn (Application $app): RootAssetManager => new RootAssetManager( $app->make(AssetManager::class) diff --git a/src/Asset/Infrastructure/Repositories/AssetContainer.php b/src/Asset/Infrastructure/Repositories/AssetContainer.php deleted file mode 100755 index 8674e9b7..00000000 --- a/src/Asset/Infrastructure/Repositories/AssetContainer.php +++ /dev/null @@ -1,102 +0,0 @@ -hotFile = $config['hot_file'] ?? $this->name.'.hot'; - $this->buildDirectory = $config['build_directory'] ?? 'build/'.$this->name; - $this->manifestPath = $config['manifest_path'] ?? 'manifest.json'; - $this->basePath = $config['base_path'] ?? ''; - } - - /** - * Gets the base path for assets in this container. - */ - public function getBasePath(): string - { - return $this->basePath; - } - - /** - * Gets the unique name of this asset container. - */ - public function getName(): string - { - return $this->name; - } - - /** - * Gets the path to the hot reload file used by Vite. - */ - public function getHotFile(): string - { - return $this->hotFile; - } - - /** - * Gets the build directory for assets. - */ - public function getBuildDirectory(): string - { - return $this->buildDirectory; - } - - /** - * Gets the path to the Vite manifest file. - */ - public function getManifestPath(): string - { - return $this->manifestPath; - } - - /** - * Gets the asset directory array. - */ - public function getAssetDir(): array - { - return $this->assetDir; - } -} diff --git a/src/Asset/Infrastructure/Repositories/InMemoryAssetRepository.php b/src/Asset/Infrastructure/Repositories/InMemoryAssetRepository.php deleted file mode 100755 index f29c9dce..00000000 --- a/src/Asset/Infrastructure/Repositories/InMemoryAssetRepository.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ - private array $assets = []; - - /** - * Finds an asset by its name/handle. - * - * @param string $name The asset name/handle - * @return Asset|null The asset instance, or null if not found - */ - public function findByName(string $name): ?Asset - { - return $this->assets[$name] ?? null; - } - - /** - * Saves an asset instance to the repository. - * - * @param Asset $asset The asset instance to save - */ - public function save(Asset $asset): void - { - $this->assets[$asset->getName()] = $asset; - } - - /** - * Retrieves all assets from the repository. - * - * @return array List of all asset instances - */ - public function all(): array - { - return array_values($this->assets); - } -} diff --git a/src/Asset/Infrastructure/Services/AssetEnqueuer.php b/src/Asset/Infrastructure/Services/AssetEnqueuer.php deleted file mode 100755 index e1e974df..00000000 --- a/src/Asset/Infrastructure/Services/AssetEnqueuer.php +++ /dev/null @@ -1,654 +0,0 @@ - - */ - protected string|array $path; - - /** - * The type of asset ('css' or 'js'). - */ - protected string $type; - - /** - * Array of asset handles that this asset depends on. - * - * @var array - */ - protected array $dependencies = []; - - /** - * The asset container instance. - */ - protected ?AssetContainer $container = null; - - /** - * The Vite manager instance. - */ - protected ?ViteManagerInterface $viteManager = null; - - /** - * Whether to use Vite.js for asset handling. - */ - protected bool $useVite = false; - - /** - * The version string for cache busting. - */ - protected ?string $version = null; - - /** - * The media query for stylesheet (e.g., 'all', 'print', 'screen'). - */ - protected string $media = 'all'; - - /** - * Whether to load the script in the footer. - */ - protected bool $loadInFooter = false; - - /** - * The loading strategy for scripts (e.g., 'defer', 'async'). - */ - protected ?string $loadStrategy = null; - - /** - * The content to be added inline with the asset. - */ - protected ?string $inlineContent = null; - - /** - * The position for inline content ('before' or 'after'). - */ - protected ?string $inlinePosition = null; - - /** - * Array of WordPress hooks where the asset should be enqueued. - * - * @var array - */ - protected array $hooks = []; - - /** - * The asset handle (unique identifier). - */ - protected string $handle; - - /** - * Stored localization data to apply after enqueuing. - * - * @var array - */ - protected array $localizationData = []; - - /** - * @param AssetManager $assetManager Resolves the containers by name - * @param HookAction|null $action Registers the enqueue callbacks on WordPress hooks - * @param HookFilter|null $filter Registers the tag and script attribute filters - * @param ViteManagerFactory|null $viteManagers Builds the Vite resolver of a container for useVite() - * @param RuntimeContext|null $runtime Tells the command line, where nothing is enqueued - * @param LoggerInterface|null $logger Where a failure of the deferred enqueue is reported - */ - public function __construct( - protected AssetManager $assetManager, - protected ?HookAction $action = null, - protected ?HookFilter $filter = null, - protected ?ViteManagerFactory $viteManagers = null, - protected ?RuntimeContext $runtime = null, - protected ?LoggerInterface $logger = null, - ) {} - - /** - * The hook service the enqueuer registers on. - * - * @throws AssetException When the enqueuer was built without one - */ - protected function hookAction(): HookAction - { - return $this->action ?? throw new AssetException('No hook service is set: the enqueuer was built without an Action.'); - } - - /** - * The filter service the enqueuer registers on. - * - * @throws AssetException When the enqueuer was built without one - */ - protected function hookFilter(): HookFilter - { - return $this->filter ?? throw new AssetException('No filter service is set: the enqueuer was built without a Filter.'); - } - - /** - * Sets the asset handle (unique identifier). - * - * @param string $handle Unique identifier for the asset\n */ - public function handle(string $handle): self - { - $this->handle = $handle; - - return $this; - } - - /** - * Sets the asset path and determines the type. - * - * @param string $path Path to the asset file\n */ - public function path(string $path): self - { - $this->path = $path; - $this->type = $this->determineFileType($path); - - return $this; - } - - /** - * Sets the asset container by name. - * - * @param string $containerName Name of the container - * - * @throws \RuntimeException If the container is not found - */ - public function container(string $containerName): self - { - if ($this->runtime?->isCommandLine()) { - return $this; - } - - $container = $this->assetManager->getContainer($containerName); - - if (! $container instanceof AssetContainer) { - throw new \RuntimeException(sprintf("Asset container '%s' not found. Make sure you have added it via AssetManager::addContainer().", $containerName)); - } - - $this->container = $container; - - return $this; - } - - /** - * Sets asset dependencies. - * - * @param array $dependencies Array of dependency handles\n */ - public function dependencies(array $dependencies): self - { - $this->dependencies = $dependencies; - - return $this; - } - - /** - * Enables Vite.js integration for this asset.\n * - * - * @throws \RuntimeException If no asset container is defined - */ - public function useVite(): self - { - if ($this->runtime?->isCommandLine()) { - return $this; - } - - if (! $this->container instanceof AssetContainer) { - throw new \RuntimeException("No asset container defined before useVite(). Use ->container('theme') before ->useVite()."); - } - - $this->useVite = true; - if (! $this->viteManagers instanceof ViteManagerFactory) { - throw new AssetException('No Vite resolver is set: the enqueuer was built without a ViteManagerFactory.'); - } - - $this->viteManager = $this->viteManagers->make($this->container); - - return $this; - } - - /** - * Sets the asset version for cache busting. - * - * @param string $version Version string\n */ - public function version(string $version): self - { - $this->version = $version; - - return $this; - } - - /** - * Sets the media type for stylesheets. - * - * @param string $media Media query string\n */ - public function media(string $media): self - { - $this->media = $media; - - return $this; - } - - /** - * Sets script to load in the footer.\n */ - public function loadInFooter(): self - { - $this->loadInFooter = true; - - return $this; - } - - /** - * Sets the loading strategy for scripts. - * - * @param string $strategy Loading strategy (e.g., 'defer', 'async')\n */ - public function loadStrategy(string $strategy): self - { - $this->loadStrategy = $strategy; - - return $this; - } - - /** - * Sets the asset type manually. - * - * @param string $type The asset type ('css' or 'js')\n */ - public function setType(string $type): self - { - $this->type = $type; - - return $this; - } - - /** - * Enqueues the asset in WordPress frontend (wp_enqueue_scripts).\n */ - public function toFrontend(): self - { - return $this->addHook('wp_enqueue_scripts'); - } - - /** - * Enqueues the asset in WordPress admin area (admin_enqueue_scripts).\n */ - public function toBackend(): self - { - return $this->addHook('admin_enqueue_scripts'); - } - - /** - * Enqueues the asset on the login screen (login_enqueue_scripts).\n */ - public function toLoginScreen(): self - { - return $this->addHook('login_enqueue_scripts'); - } - - /** - * Enqueues the asset in the customizer preview (customize_preview_init).\n */ - public function toCustomizer(): self - { - return $this->addHook('customize_preview_init'); - } - - /** - * Enqueues the asset in the block editor (enqueue_block_editor_assets).\n */ - public function toEditor(): self - { - return $this->addHook('enqueue_block_editor_assets'); - } - - /** - * Localizes a JavaScript file with data. - * - * @param string $objectName JavaScript object name - * @param array $data Data to localize\n */ - public function localize(string $objectName, array $data): self - { - if ($this->type === 'js') { - $this->localizationData[$objectName] = $data; - } - - return $this; - } - - /** - * Adds inline content to the asset. - * - * @param string $content Inline CSS/JS content - * @param string $position Position ('before' or 'after')\n */ - public function inline(string $content, string $position = 'after'): self - { - $this->inlineContent = $content; - $this->inlinePosition = $position; - - return $this; - } - - /** - * Handles the actual enqueuing of the asset. - * Called automatically when the object is destroyed. - */ - public function __destruct() - { - try { - $this->hooks = $this->hooks !== [] ? $this->hooks : ['wp_enqueue_scripts']; - - if ($this->useVite) { - $this->configureViteAssets(); - } - - foreach ($this->hooks as $hook) { - if ($this->needToLoadViteClient()) { - $this->loadViteClient($hook); - } - - $this->hookAction()->add($hook, fn () => $this->enqueueStyleOrScript($hook), 99); - } - } catch (\Throwable $throwable) { - $this->report('Error in AssetEnqueuer::__destruct', ['error' => $throwable->getMessage(), 'hooks' => $this->hooks, 'path' => $this->path ?? null]); - } - } - - /** - * Report a failure of the deferred enqueue. - * - * Called from the destructor, which may run when the application is already gone - * (end of the request, a test that built the enqueuer): a logger that cannot log then must not throw. - * - * @param array $context - */ - private function report(string $message, array $context): void - { - try { - ($this->logger ?? new NullLogger)->error($message, $context); - } catch (\Throwable) { - // Nothing left to report to - } - } - - /** - * Enqueues all styles and scripts for the current asset. - */ - public function enqueueStyleOrScript(?string $hook = null): void - { - $paths = $this->getAssetPaths(); - foreach ($paths as $type => $pathList) { - foreach ($pathList as $path) { - $this->enqueueAsset((string) $type, $this->forceFullUrl($path), $hook); - } - } - } - - /** - * Adds a WordPress hook for asset enqueuing. - * - * @param string $hook WordPress hook name\n */ - protected function addHook(string $hook): self - { - $this->hooks[] = $hook; - - return $this; - } - - /** - * Loads the Vite client script when in development mode. - * - * @param string $hook WordPress hook to attach the client script - */ - protected function loadViteClient(string $hook): void - { - if ($this->printsScriptModules($hook)) { - // The client is a module too: printed ahead of the import map, it would void it. - $this->hookAction()->add($hook, function (): void { - $url = $this->viteManager instanceof ViteManagerInterface ? $this->viteManager->clientUrl() : ''; - - if ($url !== '') { - // No version: the modules Vite serves import /@vite/client by its bare URL, - // and a ?ver= copy would be a second client, with its own HMR connection. - wp_enqueue_script_module('vite-client/'.md5($url), $url, [], null); - } - }, 1); - - return; - } - - $this->hookAction()->add($hook, function (): void { - if ($this->viteManager instanceof ViteManagerInterface && $this->viteManager->isRunningHot()) { - echo $this->viteManager->getViteClientHtml(); - } - }, 1); - } - - /** - * Configures asset paths for Vite integration. - * - * @throws AssetException When Vite manager is not initialized - */ - public function configureViteAssets(): void - { - if (! $this->viteManager instanceof ViteManagerInterface) { - throw new AssetException('Vite manager not initialized. Call useVite() first.'); - } - - $asset = $this->viteManager->isRunningHot() - ? $this->viteManager->asset($this->path) - : $this->viteManager->getAssetUrls([$this->path]); - $this->path = $asset; - } - - /** - * Determines if the Vite client needs to be loaded. - * - * @return bool True if Vite client should be loaded - */ - protected function needToLoadViteClient(): bool - { - return $this->useVite && $this->viteManager instanceof ViteManagerInterface && $this->viteManager->isRunningHot(); - } - - /** - * Gets the processed asset paths based on Vite configuration. - * - * @return array Array of asset paths grouped by type - */ - protected function getAssetPaths(): array - { - if ($this->useVite && ! $this->viteManager->isRunningHot()) { - - return $this->path; - } - - return [$this->type => [$this->path]]; - } - - /** - * Enqueues an individual asset based on its type. - * - * @param string $type Asset type (css or js) - * @param string $path Asset path - * - * @throws \InvalidArgumentException When asset type is not supported - */ - protected function enqueueAsset(string $type, string $path, ?string $hook = null): void - { - $handle = $this->useVite && ! $this->viteManager->isRunningHot() ? $this->handle.'/'.sanitize_title(basename($path)) : $this->handle; - match ($type) { - 'css' => $this->enqueueStyle($path, $handle), - 'js' => $this->useVite && $this->printsScriptModules($hook) - ? $this->enqueueScriptModule($path, $handle) - : $this->enqueueScript($path, $handle), - default => throw new \InvalidArgumentException('Unsupported asset type: '.$type) - }; - } - - /** - * Enqueues a JavaScript file with WordPress. - * - * @param string $path Path to the JavaScript file - */ - protected function enqueueScript(string $path, string $handle): void - { - wp_enqueue_script($handle, $path, $this->dependencies, $this->version, $this->loadInFooter); - - foreach ($this->localizationData as $objectName => $data) { - wp_localize_script($handle, $objectName, $data); - } - - if ($this->useVite) { - $this->addViteScriptAttributes($handle); - } - - if (! in_array($this->loadStrategy, [null, '', '0'], true)) { - wp_script_add_data($handle, 'defer', true); - } - - if (! in_array($this->inlineContent, [null, '', '0'], true)) { - wp_add_inline_script($handle, $this->inlineContent, $this->inlinePosition); - } - } - - /** - * Whether a Vite entry enqueued on this hook is printed by WordPress as a script module. - */ - protected function printsScriptModules(?string $hook): bool - { - return in_array($hook, self::SCRIPT_MODULE_HOOKS, true) && function_exists('wp_enqueue_script_module'); - } - - /** - * Enqueues a Vite entry as a WordPress script module. - * - * A module can only depend on modules, and takes no localized data or inline - * script. What the asset declares of those goes on a classic companion script, - * `{handle}-data`, printed in the head: it runs before the module, which the - * browser defers. - */ - protected function enqueueScriptModule(string $path, string $handle): void - { - // null, not false: false appends WordPress's version, and a module is identified by its - // exact URL — a chunk importing the entry back would load a second copy of it. - wp_enqueue_script_module($handle, $path, [], $this->version, ['in_footer' => $this->loadInFooter]); - - $this->hookFilter()->add('wp_script_attributes', fn (array $attributes): array => ($attributes['id'] ?? null) === $handle.'-js-module' - ? [...$attributes, 'crossorigin' => true] - : $attributes); - - $hasInlineContent = ! in_array($this->inlineContent, [null, '', '0'], true); - - if ($this->dependencies === [] && $this->localizationData === [] && ! $hasInlineContent) { - return; - } - - $companion = $handle.'-data'; - // No source: a registered handle that prints only its dependencies and inline data. - wp_register_script($companion, false, $this->dependencies, $this->version, false); - wp_enqueue_script($companion); - - foreach ($this->localizationData as $objectName => $data) { - wp_localize_script($companion, $objectName, $data); - } - - if ($hasInlineContent) { - wp_add_inline_script($companion, $this->inlineContent, $this->inlinePosition); - } - } - - /** - * Enqueues a CSS file with WordPress. - * - * @param string $path Path to the CSS file - * @param string $handle Unique identifier for the stylesheet - */ - protected function enqueueStyle(string $path, string $handle): void - { - wp_enqueue_style($handle, $path, $this->dependencies, $this->version, $this->media); - if (! in_array($this->inlineContent, [null, '', '0'], true)) { - wp_add_inline_style($handle, $this->inlineContent); - } - } - - /** - * Adds module and crossorigin attributes to Vite script tags. - */ - protected function addViteScriptAttributes(string $handle): void - { - $this->hookFilter()->add('script_loader_tag', fn ($tag, $scriptHandle, $src) => $scriptHandle === $handle - ? '' - : $tag, 10, 3); - } - - /** - * Ensures a full URL is used for the asset path. - * - * @param string $path Asset path - * @return string Full URL to the asset - */ - protected function forceFullUrl(string $path): string - { - if (str_contains($path, '://')) { - return $path; - } - - $basePath = $this->container instanceof AssetContainer ? $this->container->getBasePath() : ''; - $fullPath = $basePath.'/'.ltrim($path, '/'); - - return home_url($fullPath); - } - - /** - * Determines the file type from the path extension. - * - * @param string $path File path - * @return string File type (css or js) - * - * @throws \InvalidArgumentException When file type is not supported - */ - protected function determineFileType(string $path): string - { - $type = pathinfo($path, PATHINFO_EXTENSION); - - // JSX/TSX/TS are JavaScript variants (transpiled by Vite) - if (in_array($type, ['jsx', 'tsx', 'ts'], true)) { - return 'js'; - } - - if (! in_array($type, ['css', 'js'], true)) { - throw new \InvalidArgumentException('Unsupported file type: '.$type); - } - - return $type; - } -} diff --git a/src/Asset/Infrastructure/Services/AssetFile.php b/src/Asset/Infrastructure/Services/AssetFile.php deleted file mode 100755 index b77e1d8f..00000000 --- a/src/Asset/Infrastructure/Services/AssetFile.php +++ /dev/null @@ -1,47 +0,0 @@ -filename, $this->assetContainer) : ''; - } -} diff --git a/src/Asset/UI/Http/MissingBuildNotice.php b/src/Asset/UI/Http/MissingBuildNotice.php deleted file mode 100644 index b57099e3..00000000 --- a/src/Asset/UI/Http/MissingBuildNotice.php +++ /dev/null @@ -1,72 +0,0 @@ -missingBuilds->all(); - - if ($missing === [] || ! current_user_can('manage_options')) { - return; - } - - $ddev = getenv('IS_DDEV_PROJECT') === 'true'; - $build = $ddev ? 'ddev npm install && ddev npm run build' : 'npm install && npm run build'; - $doctor = ($ddev ? 'ddev exec ' : '').'php artisan pollora:doctor'; - - // A theme's blocks share its build: one line per manifest - $containersByManifest = []; - foreach ($missing as $container => $manifest) { - $containersByManifest[$manifest][] = $container; - } - - $items = ''; - foreach ($containersByManifest as $manifest => $containers) { - $items .= sprintf( - '
  • %s — %s %s
  • ', - esc_html(implode(', ', $containers)), - esc_html__('no manifest at', 'pollora'), - esc_html($this->relative($manifest)) - ); - } - - printf( - '

    %s

    %s

      %s

    %s
    %s

    %s
    %s

    ', - esc_html__('Pollora: assets not built', 'pollora'), - esc_html__('These Vite builds are missing, so their scripts and styles are left out: the site renders unstyled and blocks have no editor scripts.', 'pollora'), - $items, - esc_html__('Build them from the folder of the theme, plugin or module:', 'pollora'), - esc_html($build), - esc_html__('Not sure which folder? The builds check names it:', 'pollora'), - esc_html($doctor) - ); - } - - private function relative(string $path): string - { - if ($this->root === '') { - return $path; - } - - $base = rtrim($this->root, '/').'/'; - - return str_starts_with($path, $base) ? substr($path, strlen($base)) : $path; - } -} diff --git a/tests/Feature/Asset/AssetEnqueuerTest.php b/tests/Feature/Asset/AssetEnqueuerTest.php deleted file mode 100644 index c7601ac0..00000000 --- a/tests/Feature/Asset/AssetEnqueuerTest.php +++ /dev/null @@ -1,415 +0,0 @@ -shouldReceive('isConsole')->andReturn(true); - $detector->shouldReceive('isWpCli')->andReturn(false); - - $this->app->instance(ConsoleDetectorInterface::class, $detector); - $this->app->singleton(ConsoleDetectionService::class, fn (): ConsoleDetectionService => new ConsoleDetectionService($detector)); - $this->app->singleton(RuntimeContext::class, fn (): RuntimeContext => new LaravelRuntimeContext(new ConsoleDetectionService($detector))); - - // Bind AssetManager - $this->assetManager = Mockery::mock(AssetManager::class); - $this->app->instance(AssetManager::class, $this->assetManager); - - // Bind HookAction to prevent __destruct from failing - $this->hookAction = Mockery::mock(HookAction::class); - $this->hookAction->shouldReceive('add')->byDefault(); - $this->app->instance(HookAction::class, $this->hookAction); -}); - -describe('AssetEnqueuer', function (): void { - describe('fluent builder', function (): void { - it('chains handle and path', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->handle('my-script')->path('assets/app.js'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains dependencies', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->dependencies(['jquery']); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains version', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->version('1.2.3'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains media', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->media('print'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains loadInFooter', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->loadInFooter(); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains loadStrategy', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->loadStrategy('defer'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains setType', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->setType('css'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('chains inline', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->inline('body { color: red; }', 'after'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - }); - - describe('context hooks', function (): void { - it('toFrontend sets wp_enqueue_scripts hook', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - expect($enqueuer->toFrontend())->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('toBackend sets admin_enqueue_scripts hook', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - expect($enqueuer->toBackend())->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('toLoginScreen sets login_enqueue_scripts hook', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - expect($enqueuer->toLoginScreen())->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('toCustomizer sets customize_preview_init hook', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - expect($enqueuer->toCustomizer())->toBeInstanceOf(AssetEnqueuer::class); - }); - - it('toEditor sets enqueue_block_editor_assets hook', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - expect($enqueuer->toEditor())->toBeInstanceOf(AssetEnqueuer::class); - }); - }); - - describe('determineFileType', function (): void { - it('detects CSS files', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('assets/style.css'); - - $reflection = new ReflectionProperty($enqueuer, 'type'); - expect($reflection->getValue($enqueuer))->toBe('css'); - }); - - it('detects JS files', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('assets/app.js'); - - $reflection = new ReflectionProperty($enqueuer, 'type'); - expect($reflection->getValue($enqueuer))->toBe('js'); - }); - - it('treats JSX as JS', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('components/App.jsx'); - - $reflection = new ReflectionProperty($enqueuer, 'type'); - expect($reflection->getValue($enqueuer))->toBe('js'); - }); - - it('treats TSX as JS', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('components/App.tsx'); - - $reflection = new ReflectionProperty($enqueuer, 'type'); - expect($reflection->getValue($enqueuer))->toBe('js'); - }); - - it('treats TS as JS', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('assets/app.ts'); - - $reflection = new ReflectionProperty($enqueuer, 'type'); - expect($reflection->getValue($enqueuer))->toBe('js'); - }); - - it('throws on unsupported file type', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - expect(fn () => $enqueuer->path('image.png')) - ->toThrow(InvalidArgumentException::class); - }); - }); - - describe('localize', function (): void { - it('stores localization data for JS assets', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('app.js'); - - $result = $enqueuer->localize('myData', ['key' => 'value']); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - - $reflection = new ReflectionProperty($enqueuer, 'localizationData'); - expect($reflection->getValue($enqueuer))->toHaveKey('myData'); - }); - - it('ignores localization for CSS assets', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - $enqueuer->path('style.css'); - - $enqueuer->localize('myData', ['key' => 'value']); - - $reflection = new ReflectionProperty($enqueuer, 'localizationData'); - expect($reflection->getValue($enqueuer))->toBeEmpty(); - }); - }); - - describe('container', function (): void { - it('skips in console mode', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->container('theme'); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - - $reflection = new ReflectionProperty($enqueuer, 'container'); - expect($reflection->getValue($enqueuer))->toBeNull(); - }); - }); - - describe('useVite', function (): void { - it('skips in console mode', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer->useVite(); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - - $reflection = new ReflectionProperty($enqueuer, 'useVite'); - expect($reflection->getValue($enqueuer))->toBeFalse(); - }); - }); - - describe('the Vite resolver of useVite()', function (): void { - beforeEach(function (): void { - // Not the command line: useVite() acts - $this->app->singleton(RuntimeContext::class, fn (): RuntimeContext => new class implements RuntimeContext - { - public function isCommandLine(): bool - { - return false; - } - }); - - $this->container = new AssetContainer('theme', ['hot_file' => '/tmp/none.hot', 'build_directory' => 'build/theme/x']); - $this->assetManager->shouldReceive('getContainer')->with('theme')->andReturn($this->container); - }); - - it('asks the factory for the resolver of the container it was given', function (): void { - $resolver = Mockery::mock(ViteManagerInterface::class)->shouldIgnoreMissing(); - $factory = Mockery::mock(ViteManagerFactory::class); - $factory->shouldReceive('make')->once()->with($this->container)->andReturn($resolver); - $this->app->instance(ViteManagerFactory::class, $factory); - - $enqueuer = resolve(AssetEnqueuer::class)->handle('theme/script')->path('app.js')->container('theme')->useVite(); - - expect((new ReflectionProperty($enqueuer, 'viteManager'))->getValue($enqueuer))->toBe($resolver); - }); - - it('says so when no resolver is set', function (): void { - $this->app->forgetInstance(ViteManagerFactory::class); - - expect(fn () => resolve(AssetEnqueuer::class)->handle('theme/script')->path('app.js')->container('theme')->useVite()) - ->toThrow(AssetException::class, 'No Vite resolver is set'); - }); - }); - - describe('Vite entries as script modules', function (): void { - beforeEach(function (): void { - $this->filters = []; - $hookFilter = Mockery::mock(HookFilter::class); - $hookFilter->shouldReceive('add')->andReturnUsing(function (string $hook, Closure $callback) use ($hookFilter): HookFilter { - $this->filters[$hook] = $callback; - - return $hookFilter; - }); - $this->app->instance(HookFilter::class, $hookFilter); - - $this->modules = []; - $this->scripts = []; - $this->registered = []; - Functions\when('wp_enqueue_script_module')->alias(function (string $id, string $src = '', array $deps = [], $version = false, array $args = []): void { - $this->modules[$id] = ['src' => $src, 'deps' => $deps, 'version' => $version, 'args' => $args]; - }); - Functions\when('wp_register_script')->alias(function (string $handle, $src, array $deps = []): void { - $this->registered[$handle] = ['src' => $src, 'deps' => $deps]; - }); - Functions\when('wp_enqueue_script')->alias(function (string $handle, $src = '', array $deps = []): void { - $this->scripts[$handle] = ['src' => $src, 'deps' => $deps]; - }); - Functions\when('sanitize_title')->alias(fn (string $title): string => strtolower(str_replace('.', '-', $title))); - - $this->viteManager = Mockery::mock(ViteManager::class); - $this->viteManager->shouldReceive('isRunningHot')->andReturn(false)->byDefault(); - $this->viteManager->shouldReceive('getAssetUrls')->andReturn([])->byDefault(); - }); - - /** An enqueuer for a built Vite entry, set up the way useVite() would outside the console. */ - function viteEnqueuer(ViteManager $viteManager, array $settings = []): AssetEnqueuer - { - $enqueuer = resolve(AssetEnqueuer::class)->handle('buzz/script'); - - foreach (['useVite' => true, 'viteManager' => $viteManager, 'path' => ['js' => ['https://site.test/build/app-abc.js']], ...$settings] as $property => $value) { - (new ReflectionProperty($enqueuer, $property))->setValue($enqueuer, $value); - } - - return $enqueuer; - } - - it('enqueues the entry as a module on the front end, with no version appended', function (): void { - viteEnqueuer($this->viteManager)->enqueueStyleOrScript('wp_enqueue_scripts'); - - expect($this->modules)->toHaveKey('buzz/script/app-abc-js') - ->and($this->modules['buzz/script/app-abc-js']) - ->toMatchArray(['src' => 'https://site.test/build/app-abc.js', 'version' => null, 'args' => ['in_footer' => false]]) - ->and($this->scripts)->toBe([]); - }); - - it('passes loadInFooter() on as in_footer, as WordPress reads it for its own modules', function (): void { - viteEnqueuer($this->viteManager, ['loadInFooter' => true])->enqueueStyleOrScript('wp_enqueue_scripts'); - - expect($this->modules['buzz/script/app-abc-js']['args'])->toBe(['in_footer' => true]); - }); - - it('enqueues the entry as a module in the admin', function (): void { - viteEnqueuer($this->viteManager)->enqueueStyleOrScript('admin_enqueue_scripts'); - - expect($this->modules)->toHaveKey('buzz/script/app-abc-js'); - }); - - it('keeps a classic script where WordPress prints no modules, such as the editor', function (): void { - viteEnqueuer($this->viteManager)->enqueueStyleOrScript('enqueue_block_editor_assets'); - - expect($this->modules)->toBe([]) - ->and($this->scripts)->toHaveKey('buzz/script/app-abc-js'); - }); - - it('marks the module tag crossorigin, and no other tag', function (): void { - viteEnqueuer($this->viteManager)->enqueueStyleOrScript('wp_enqueue_scripts'); - - $filter = $this->filters['wp_script_attributes']; - - expect($filter(['id' => 'buzz/script/app-abc-js-js-module']))->toHaveKey('crossorigin', true) - ->and($filter(['id' => 'something-else-js-module']))->not->toHaveKey('crossorigin'); - }); - - it('puts dependencies, localized data and inline script on a classic companion', function (): void { - $localized = []; - $inline = []; - Functions\when('wp_localize_script')->alias(function (string $handle, string $name) use (&$localized): void { - $localized[$handle][] = $name; - }); - Functions\when('wp_add_inline_script')->alias(function (string $handle, string $code) use (&$inline): void { - $inline[$handle] = $code; - }); - - viteEnqueuer($this->viteManager, [ - 'dependencies' => ['jquery'], - 'localizationData' => ['buzzData' => ['a' => 1]], - 'inlineContent' => 'window.ready = true;', - ])->enqueueStyleOrScript('wp_enqueue_scripts'); - - expect($this->modules['buzz/script/app-abc-js']['deps'])->toBe([]) - ->and($this->registered['buzz/script/app-abc-js-data'])->toBe(['src' => false, 'deps' => ['jquery']]) - ->and($this->scripts)->toHaveKey('buzz/script/app-abc-js-data') - ->and($localized)->toBe(['buzz/script/app-abc-js-data' => ['buzzData']]) - ->and($inline)->toBe(['buzz/script/app-abc-js-data' => 'window.ready = true;']); - }); - - it('enqueues the Vite client as a module while Vite runs hot', function (): void { - $this->viteManager->shouldReceive('isRunningHot')->andReturn(true); - $this->viteManager->shouldReceive('clientUrl')->andReturn('https://site.test:5173/@vite/client'); - - $callbacks = []; - $hookAction = $this->hookAction; - $hookAction->shouldReceive('add')->andReturnUsing(function (string $hook, callable $callback, int $priority = 10) use (&$callbacks, $hookAction): HookAction { - $callbacks[] = [$hook, $priority, $callback]; - - return $hookAction; - }); - - (function (): void { - $this->loadViteClient('wp_enqueue_scripts'); - })->call(viteEnqueuer($this->viteManager)); - - [$hook, $priority, $callback] = $callbacks[0]; - $callback(); - - expect([$hook, $priority])->toBe(['wp_enqueue_scripts', 1]) - ->and(array_column($this->modules, 'src'))->toBe(['https://site.test:5173/@vite/client']) - ->and(array_column($this->modules, 'version'))->toBe([null]); - }); - }); - - describe('full chain', function (): void { - it('supports complete fluent configuration', function (): void { - $enqueuer = $this->app->make(AssetEnqueuer::class); - - $result = $enqueuer - ->handle('my-app') - ->path('assets/app.js') - ->dependencies(['jquery']) - ->version('1.0.0') - ->loadInFooter() - ->loadStrategy('defer') - ->toFrontend(); - - expect($result)->toBeInstanceOf(AssetEnqueuer::class); - }); - }); -}); diff --git a/tests/Feature/Asset/AssetManagerContainersTest.php b/tests/Feature/Asset/AssetManagerContainersTest.php deleted file mode 100644 index 5b8a0473..00000000 --- a/tests/Feature/Asset/AssetManagerContainersTest.php +++ /dev/null @@ -1,23 +0,0 @@ -addContainer('theme', ['hot_file' => 'theme.hot']); - $manager->addContainer('acme-plugin', []); - - $containers = $manager->containers(); - - expect(array_keys($containers))->toBe(['theme', 'acme-plugin']) - ->and($containers['theme'])->toBeInstanceOf(AssetContainer::class) - ->and($containers['theme']->getName())->toBe('theme'); -}); - -it('lists nothing before a container is added', function (): void { - expect((new AssetManager(Mockery::mock(AssetRetrievalService::class)))->containers())->toBe([]); -}); diff --git a/tests/Feature/Asset/ViteManagerTest.php b/tests/Feature/Asset/ViteManagerTest.php index 735edffd..3920cb7a 100644 --- a/tests/Feature/Asset/ViteManagerTest.php +++ b/tests/Feature/Asset/ViteManagerTest.php @@ -6,6 +6,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Log; use Pollora\Asset\Application\Services\MissingBuilds; +use Pollora\Asset\Infrastructure\Providers\AssetServiceProvider; use Pollora\Asset\Infrastructure\Repositories\AssetContainer; use Pollora\Asset\Infrastructure\Services\ViteManager; use Pollora\Asset\UI\Http\MissingBuildNotice; @@ -132,8 +133,8 @@ describe('MissingBuildNotice', function (): void { beforeEach(function (): void { - // AssetServiceProvider's binding: manifest paths shown from the project root - app()->bind(MissingBuildNotice::class, fn (): MissingBuildNotice => new MissingBuildNotice(resolve(MissingBuilds::class), base_path())); + // The notice as the provider builds it: manifest paths from the project root, and the doctor command + app()->register(AssetServiceProvider::class); Functions\when('current_user_can')->justReturn(true); Functions\when('esc_html')->returnArg(); Functions\when('esc_html__')->returnArg(); diff --git a/tests/Unit/Asset/AssetDomainTest.php b/tests/Unit/Asset/AssetDomainTest.php deleted file mode 100644 index f56d63fc..00000000 --- a/tests/Unit/Asset/AssetDomainTest.php +++ /dev/null @@ -1,55 +0,0 @@ - 'js']); - expect($asset->getName())->toBe('main'); - expect($asset->getPath())->toBe('assets/main.js'); - expect($asset->getAttributes())->toBe(['type' => 'js']); - }); -}); - -describe('AssetFile domain model', function (): void { - it('can be instantiated and return filename and container', function (): void { - $file = new AssetFile('assets/app.css'); - expect($file->getFilename())->toBe('assets/app.css'); - expect($file->getAssetContainer())->toBe('theme'); - }); - it('can set a custom asset container', function (): void { - $file = (new AssetFile('assets/app.css'))->from('custom'); - expect($file->getAssetContainer())->toBe('custom'); - }); - it('can be cast to string as filename', function (): void { - $file = new AssetFile('assets/app.css'); - expect((string) $file)->toBe('assets/app.css'); - }); -}); - -describe('ViteManager domain stub', function (): void { - beforeEach(function (): void { - $app = Mockery::mock(Container::class)->makePartial(); - $app->shouldReceive('publicPath')->andReturnUsing(fn ($path = ''): string => '/tmp/public'.($path ? '/'.$path : '')); - $app->instance('path.public', '/tmp/public'); - Container::setInstance($app); - }); - - afterEach(function (): void { - Container::setInstance(new Container); - }); - it('returns stub values for all interface methods', function (): void { - $vite = new ViteManager; - expect($vite->container())->toBeInstanceOf(AssetContainer::class); - expect($vite->getAssetUrls(['entry.js']))->toBeArray()->toBeEmpty(); - expect($vite->asset('foo.js'))->toBe('foo.js'); - expect($vite->isRunningHot())->toBeFalse(); - expect($vite->getViteClientHtml())->toBe(''); - }); -});