diff --git a/CHANGELOG.md b/CHANGELOG.md
index a270a05..fbd135e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,3 +9,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- feat: added a framework-agnostic Vite facade with explicit development and production configuration.
- docs: add class-level PHPDoc for the framework-neutral Vite APIs.
+- refactor: remove unreachable renderer and manifest resolver branches, retain filesystem race protection, and reach `100%` code coverage without reflection-based tests.
diff --git a/composer.json b/composer.json
index 0d4470e..b6c6671 100644
--- a/composer.json
+++ b/composer.json
@@ -40,7 +40,8 @@
"phpstan/phpstan": "^2.2",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0.3",
- "phpunit/phpunit": "^12.5"
+ "phpunit/phpunit": "^12.5",
+ "xepozz/internal-mocker": "^1.4"
},
"autoload": {
"psr-4": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 4d399f6..9e06a44 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -10,6 +10,10 @@
failOnWarning="true"
stopOnFailure="false"
>
+
+
+
+
tests
diff --git a/src/Asset/AssetCollection.php b/src/Asset/AssetCollection.php
index abde8a2..4117ef1 100644
--- a/src/Asset/AssetCollection.php
+++ b/src/Asset/AssetCollection.php
@@ -16,17 +16,18 @@
/**
* Immutable, insertion-ordered collection of deduplicated neutral assets.
*
- * @implements IteratorAggregate
+ * @implements IteratorAggregate
*/
final readonly class AssetCollection implements Countable, IteratorAggregate
{
/**
- * @var list Accepted assets in insertion order, with per-type duplicates removed.
+ * @var list Accepted assets in insertion order, with per-type
+ * duplicates removed.
*/
private array $assets;
/**
- * @param iterable $assets Assets to collect.
+ * @param iterable $assets Assets to validate and collect.
*
* @throws ConfigurationException if a value does not implement {@see AssetInterface}, or implements it without
* being one of the four supported asset types.
@@ -57,7 +58,7 @@ public function __construct(iterable $assets = [])
/**
* Returns every collected asset in insertion order, regardless of type.
*
- * @return list The collected assets.
+ * @return list The collected assets.
*/
public function all(): array
{
@@ -93,7 +94,7 @@ public function count(): int
/**
* Iterates over the collected assets in insertion order.
*
- * @return Traversable Iterator over the collected assets.
+ * @return Traversable Iterator over the collected assets.
*/
public function getIterator(): Traversable
{
@@ -198,7 +199,7 @@ public function stylesheets(): array
*
* @return InlineModule|ModulePreload|ModuleScript|Stylesheet The narrowed asset.
*/
- private function requireAsset(mixed $asset): AssetInterface
+ private function requireAsset(mixed $asset): InlineModule|ModulePreload|ModuleScript|Stylesheet
{
if (!$asset instanceof AssetInterface) {
throw new ConfigurationException(
diff --git a/src/Configuration/DevelopmentConfiguration.php b/src/Configuration/DevelopmentConfiguration.php
index 622dc8c..bd5d749 100644
--- a/src/Configuration/DevelopmentConfiguration.php
+++ b/src/Configuration/DevelopmentConfiguration.php
@@ -26,8 +26,8 @@
/**
* @param string $devServerUrl Absolute HTTP(S) URL of the running Vite development server.
* @param bool $includeViteClient Whether the `@vite/client` module script is emitted.
- * @param list $inlineModuleProviders Providers of application-owned inline
- * modules that must run before the Vite client.
+ * @param list $inlineModuleProviders Providers of application-owned inline modules to validate and emit
+ * before the Vite client.
*
* @throws ConfigurationException if the development-server URL is not an absolute HTTP(S) URL, or if a provider
* does not implement {@see InlineModuleProviderInterface}.
diff --git a/src/Exception/Message.php b/src/Exception/Message.php
index a305d5f..06d1bac 100644
--- a/src/Exception/Message.php
+++ b/src/Exception/Message.php
@@ -168,13 +168,6 @@ enum Message: string
*/
case HTML_ATTRIBUTE_VALUE_INVALID = 'The HTML attribute "%s" has an unsupported value.';
- /**
- * An inline module cannot be rendered safely.
- *
- * Format: "Unable to render the inline module source."
- */
- case INLINE_MODULE_RENDER_FAILED = 'Unable to render the inline module source.';
-
/**
* An inline module has no source.
*
@@ -273,13 +266,6 @@ enum Message: string
*/
case MANIFEST_REFERENCE_MISSING = 'The Vite manifest entry "%s" in "%s" references missing "%s" chunk "%s".';
- /**
- * A resolved manifest import is missing unexpectedly.
- *
- * Format: "The Vite manifest entry \"%s\" in \"%s\" references missing chunk \"%s\"."
- */
- case MANIFEST_RESOLVER_REFERENCE_MISSING = 'The Vite manifest entry "%s" in "%s" references missing chunk "%s".';
-
/**
* A manifest root is not a JSON object.
*
diff --git a/src/Html/HtmlRenderOptions.php b/src/Html/HtmlRenderOptions.php
index 3f69a3f..a311b5f 100644
--- a/src/Html/HtmlRenderOptions.php
+++ b/src/Html/HtmlRenderOptions.php
@@ -18,19 +18,19 @@
final readonly class HtmlRenderOptions
{
/**
- * @var (Closure(AssetInterface): array)|null Per-asset attribute callback,
- * or `null` when only the static per-type attributes apply.
+ * @var (Closure(AssetInterface): mixed)|null Per-asset attribute callback, or `null` when only the static per-type
+ * attributes apply.
*/
private Closure|null $attributeProvider;
/**
* @param string|null $nonce CSP nonce applied to every generated tag, or `null` to emit none.
- * @param array $moduleScriptAttributes Extra attributes for module scripts.
- * @param array $stylesheetAttributes Extra attributes for stylesheets.
- * @param array $modulePreloadAttributes Extra attributes for preload hints.
- * @param array $inlineModuleAttributes Extra attributes for inline modules.
- * @param (callable(AssetInterface): array)|null $attributeProvider Callback
- * returning per-asset attributes that override the per-type ones, or `null` to apply none.
+ * @param array $moduleScriptAttributes Extra attributes for module scripts.
+ * @param array $stylesheetAttributes Extra attributes for stylesheets.
+ * @param array $modulePreloadAttributes Extra attributes for preload hints.
+ * @param array $inlineModuleAttributes Extra attributes for inline modules.
+ * @param (callable(AssetInterface): mixed)|null $attributeProvider Callback returning per-asset attributes that
+ * override the per-type ones, or `null` to apply none.
* @param string $separator String inserted between two rendered tags.
*
* @throws HtmlRenderingException if the nonce is not a non-empty base64 or base64url value.
@@ -95,10 +95,9 @@ public function attributesFor(AssetInterface $asset): array
/**
* Invokes the attribute provider without narrowing its result.
*
- * The return type stays `mixed` so the caller can reject a provider that breaks its declared contract at
- * runtime, which static analysis alone cannot guarantee.
+ * The return type stays `mixed` so the caller can validate the provider result at runtime.
*
- * @param Closure(AssetInterface): array $provider Configured callback.
+ * @param Closure(AssetInterface): mixed $provider Configured callback.
* @param AssetInterface $asset Asset passed to the callback.
*
* @return mixed Whatever the provider returned.
diff --git a/src/Html/HtmlRenderer.php b/src/Html/HtmlRenderer.php
index b9e96b7..4b10ffc 100644
--- a/src/Html/HtmlRenderer.php
+++ b/src/Html/HtmlRenderer.php
@@ -15,7 +15,7 @@
use function is_int;
use function is_string;
use function preg_match;
-use function preg_replace;
+use function str_ireplace;
use function str_starts_with;
use function strtolower;
@@ -47,8 +47,8 @@ final class HtmlRenderer
* @param AssetCollection $assets Resolved assets to render.
* @param HtmlRenderOptions|null $options Per-render policy, or `null` to apply the defaults.
*
- * @throws HtmlRenderingException if an asset type is unsupported, an inline module cannot be neutralized, or a
- * custom attribute is malformed, reserved, duplicated, or carries an unsupported value.
+ * @throws HtmlRenderingException if a custom attribute is malformed, reserved, duplicated, or carries an
+ * unsupported value.
*
* @return string The rendered tags joined by the configured separator.
*/
@@ -110,58 +110,39 @@ private function attributesFor(AssetInterface $asset, HtmlRenderOptions $options
* Module scripts and inline modules become ` Script::tag()
->attributes($this->attributesFor($asset, $options))
->type('module')
->src($asset->url)
- ->render();
- }
-
- if ($asset instanceof Stylesheet) {
- return Link::tag()
+ ->render(),
+ $asset instanceof Stylesheet => Link::tag()
->attributes($this->attributesFor($asset, $options))
->rel('stylesheet')
->href($asset->url)
- ->render();
- }
-
- if ($asset instanceof ModulePreload) {
- return Link::tag()
+ ->render(),
+ $asset instanceof ModulePreload => Link::tag()
->attributes($this->attributesFor($asset, $options))
->rel('modulepreload')
->href($asset->url)
- ->render();
- }
-
- if ($asset instanceof InlineModule) {
- $source = preg_replace('~source);
-
- if ($source === null) {
- throw new HtmlRenderingException(
- Message::INLINE_MODULE_RENDER_FAILED->getMessage(),
- );
- }
-
- return Script::tag()
+ ->render(),
+ $asset instanceof InlineModule => Script::tag()
->attributes($this->attributesFor($asset, $options))
->type('module')
- ->html($source)
- ->render();
- }
-
- throw new HtmlRenderingException(
- Message::ASSET_IMPLEMENTATION_UNSUPPORTED->getMessage(),
- );
+ ->html(str_ireplace('source))
+ ->render(),
+ };
}
/**
diff --git a/src/Manifest/ManifestLoader.php b/src/Manifest/ManifestLoader.php
index 8d443cd..8585fe2 100644
--- a/src/Manifest/ManifestLoader.php
+++ b/src/Manifest/ManifestLoader.php
@@ -18,7 +18,6 @@
use function array_is_list;
use function array_key_exists;
use function clearstatcache;
-use function file_get_contents;
use function is_array;
use function is_bool;
use function is_file;
@@ -27,7 +26,6 @@
use function json_decode;
use function property_exists;
use function sprintf;
-use function stat;
use const JSON_THROW_ON_ERROR;
diff --git a/src/Resolver/ManifestAssetResolver.php b/src/Resolver/ManifestAssetResolver.php
index e76d250..3e1a115 100644
--- a/src/Resolver/ManifestAssetResolver.php
+++ b/src/Resolver/ManifestAssetResolver.php
@@ -167,8 +167,6 @@ private function collectCss(array &$stylesheets, ManifestChunk $chunk): void
* @param ManifestChunk $chunk Chunk whose imports are walked.
* @param array $seen Keys already visited, mutated in place to guard against cycles.
*
- * @throws InvalidManifestException if an import references a chunk the manifest does not declare.
- *
* @return list Imported chunks, dependencies first.
*/
private function importedChunks(Manifest $manifest, ManifestChunk $chunk, array &$seen): array
@@ -181,17 +179,9 @@ private function importedChunks(Manifest $manifest, ManifestChunk $chunk, array
}
$seen[$reference] = true;
- $import = $manifest->get($reference);
- if (!$import instanceof ManifestChunk) {
- throw new InvalidManifestException(
- Message::MANIFEST_RESOLVER_REFERENCE_MISSING->getMessage(
- $chunk->key,
- $this->configuration->manifestPath,
- $reference,
- ),
- );
- }
+ /** @var ManifestChunk $import References are validated when the manifest is loaded. */
+ $import = $manifest->get($reference);
$chunks = [...$chunks, ...$this->importedChunks($manifest, $import, $seen), $import];
}
diff --git a/src/Vite.php b/src/Vite.php
index d53bd73..480022d 100644
--- a/src/Vite.php
+++ b/src/Vite.php
@@ -51,7 +51,7 @@
* eagerly but may be empty, since {@see Vite::resolve()} accepts a per-call override.
*
* @param DevelopmentConfiguration|ProductionConfiguration $configuration Configuration selecting the strategy.
- * @param list $entrypoints Default entrypoints resolved when no override is supplied.
+ * @param list $entrypoints Default entrypoints to validate and resolve when no override is supplied.
* @param ManifestLoader|null $manifestLoader Loader to share across instances, or `null` to create one.
*
* @throws InvalidEntrypointException if a default entrypoint is not a `string`, is empty, or contains a
diff --git a/tests/AssetCollectionTest.php b/tests/AssetCollectionTest.php
index 5832a93..d1390e6 100644
--- a/tests/AssetCollectionTest.php
+++ b/tests/AssetCollectionTest.php
@@ -9,6 +9,7 @@
use PHPForge\Vite\Tests\Fixture\UnsupportedAssetStub;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
+use stdClass;
use function iterator_to_array;
@@ -126,4 +127,14 @@ public function testThrowConfigurationExceptionForUnsupportedAssetImplementation
new AssetCollection([new UnsupportedAssetStub()]);
}
+
+ public function testThrowConfigurationExceptionForValueThatIsNotAnAsset(): void
+ {
+ $this->expectException(ConfigurationException::class);
+ $this->expectExceptionMessage(
+ Message::ASSET_COLLECTION_ITEM_INVALID->getMessage(),
+ );
+
+ new AssetCollection([new stdClass()]);
+ }
}
diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php
index 39ba37b..2701ae6 100644
--- a/tests/ConfigurationTest.php
+++ b/tests/ConfigurationTest.php
@@ -12,7 +12,6 @@
use PHPForge\Vite\Vite;
use PHPUnit\Framework\Attributes\{DataProviderExternal, Group};
use PHPUnit\Framework\TestCase;
-use ReflectionClass;
use stdClass;
/**
@@ -93,9 +92,7 @@ public function testThrowConfigurationExceptionForInvalidInlineModuleProvider():
Message::DEVELOPMENT_INLINE_MODULE_PROVIDER_INVALID->getMessage(),
);
- (new ReflectionClass(DevelopmentConfiguration::class))->newInstanceArgs(
- ['http://localhost:5173', true, [new stdClass()]],
- );
+ new DevelopmentConfiguration('http://localhost:5173', inlineModuleProviders: [new stdClass()]);
}
public function testThrowConfigurationExceptionForNonAbsoluteManifestPath(): void
@@ -160,8 +157,6 @@ public function testThrowInvalidEntrypointExceptionForNonStringEntrypoint(): voi
Message::ENTRYPOINT_TYPE_INVALID->getMessage(),
);
- (new ReflectionClass(Vite::class))->newInstanceArgs(
- [new DevelopmentConfiguration('http://localhost:5173'), [123]],
- );
+ new Vite(new DevelopmentConfiguration('http://localhost:5173'), [123]);
}
}
diff --git a/tests/Fixture/duplicate-file-manifest.json b/tests/Fixture/duplicate-file-manifest.json
index 58dd38b..cb2a69d 100644
--- a/tests/Fixture/duplicate-file-manifest.json
+++ b/tests/Fixture/duplicate-file-manifest.json
@@ -1,7 +1,14 @@
{
+ "_bundle-alias.js": {
+ "file": "assets/bundle.js"
+ },
+ "_vendor.js": {
+ "file": "assets/vendor.js"
+ },
"resources/js/app.js": {
"file": "assets/bundle.js",
- "isEntry": true
+ "isEntry": true,
+ "imports": ["_bundle-alias.js", "_vendor.js"]
},
"resources/js/app-legacy.js": {
"file": "assets/bundle.js",
diff --git a/tests/HtmlRendererTest.php b/tests/HtmlRendererTest.php
index c093702..f9ad439 100644
--- a/tests/HtmlRendererTest.php
+++ b/tests/HtmlRendererTest.php
@@ -7,10 +7,10 @@
use PHPForge\Vite\Asset\{AssetCollection, AssetInterface, InlineModule, ModulePreload, ModuleScript, Stylesheet};
use PHPForge\Vite\Exception\{HtmlRenderingException, Message};
use PHPForge\Vite\Html\{HtmlRenderOptions, HtmlRenderer};
+use PHPForge\Vite\Tests\Fixture\UnsupportedAssetStub;
use PHPForge\Vite\Tests\Provider\HtmlRendererProvider;
use PHPUnit\Framework\Attributes\{DataProviderExternal, Group};
use PHPUnit\Framework\TestCase;
-use ReflectionClass;
/**
* Unit tests for {@see HtmlRenderer} output ordering, escaping, attributes, and CSP nonce support.
@@ -149,9 +149,7 @@ public function testThrowHtmlRenderingExceptionForInvalidAttributeProviderReturn
Message::HTML_ATTRIBUTE_PROVIDER_RESULT_INVALID->getMessage(),
);
- $options = (new ReflectionClass(HtmlRenderOptions::class))->newInstanceArgs(
- [null, [], [], [], [], static fn(): string => 'invalid'],
- );
+ $options = new HtmlRenderOptions(attributeProvider: static fn(): string => 'invalid');
$options->attributesFor(new ModuleScript('/app.js'));
}
@@ -167,7 +165,7 @@ public function testThrowHtmlRenderingExceptionForInvalidNonce(): void
}
/**
- * @param array $attributes
+ * @param array $attributes
* @param list $arguments
*/
#[DataProviderExternal(HtmlRendererProvider::class, 'unsafeAttributes')]
@@ -181,10 +179,18 @@ public function testThrowHtmlRenderingExceptionForUnsafeAttribute(
$message->getMessage(...$arguments),
);
- $options = (new ReflectionClass(HtmlRenderOptions::class))->newInstanceArgs(
- [null, $attributes],
- );
+ $options = new HtmlRenderOptions(moduleScriptAttributes: $attributes);
(new HtmlRenderer())->render(new AssetCollection([new ModuleScript('/app.js')]), $options);
}
+
+ public function testThrowHtmlRenderingExceptionForUnsupportedAssetAttributes(): void
+ {
+ $this->expectException(HtmlRenderingException::class);
+ $this->expectExceptionMessage(
+ Message::ASSET_IMPLEMENTATION_UNSUPPORTED->getMessage(),
+ );
+
+ (new HtmlRenderOptions())->attributesFor(new UnsupportedAssetStub());
+ }
}
diff --git a/tests/ManifestLoaderTest.php b/tests/ManifestLoaderTest.php
index c6f6d75..346357e 100644
--- a/tests/ManifestLoaderTest.php
+++ b/tests/ManifestLoaderTest.php
@@ -15,7 +15,9 @@
use PHPForge\Vite\Tests\Provider\ManifestLoaderProvider;
use PHPUnit\Framework\Attributes\{DataProviderExternal, Group};
use PHPUnit\Framework\TestCase;
+use Xepozz\InternalMocker\MockerState;
+use function array_keys;
use function array_map;
use function chmod;
use function clearstatcache;
@@ -38,6 +40,32 @@ final class ManifestLoaderTest extends TestCase
*/
private array $temporaryFiles = [];
+ public function testClearWithoutPathDiscardsEveryCachedManifest(): void
+ {
+ $firstPath = $this->temporaryManifest(
+ '{"first.js":{"file":"assets/first.js","isEntry":true}}',
+ );
+ $secondPath = $this->temporaryManifest(
+ '{"second.js":{"file":"assets/second.js","isEntry":true}}',
+ );
+ $loader = new ManifestLoader();
+ $firstManifest = $loader->load($firstPath);
+ $secondManifest = $loader->load($secondPath);
+
+ $loader->clear();
+
+ self::assertNotSame(
+ $firstManifest,
+ $loader->load($firstPath),
+ 'Clearing every entry must discard the first cached manifest.',
+ );
+ self::assertNotSame(
+ $secondManifest,
+ $loader->load($secondPath),
+ 'Clearing every entry must discard the second cached manifest.',
+ );
+ }
+
public function testLoaderAllowsUnknownFieldsForForwardCompatibility(): void
{
$path = $this->temporaryManifest(
@@ -101,6 +129,18 @@ public function testLoaderParsesCurrentOfficialManifestFields(): void
$entry = $manifest->get('views/bar.js');
$shared = $manifest->get('_shared-B7PI925R.js');
+ self::assertSame(
+ [
+ '_shared-B7PI925R.js',
+ '_shared-ChJ_j-JJ.css',
+ 'logo.svg',
+ 'baz.js',
+ 'views/bar.js',
+ 'views/foo.js',
+ ],
+ array_keys($manifest->chunks()),
+ 'Every manifest chunk must remain available in source order.',
+ );
self::assertNotNull(
$entry,
'The entry chunk must be present.',
@@ -200,6 +240,20 @@ public function testThrowManifestNotFoundExceptionWhenManifestIsMissing(): void
(new ManifestLoader())->load($path);
}
+ public function testThrowManifestReadExceptionWhenInspectionFails(): void
+ {
+ $path = $this->temporaryManifest('{}');
+
+ MockerState::addCondition('PHPForge\\Vite\\Manifest', 'stat', [$path], false);
+
+ $this->expectException(ManifestReadException::class);
+ $this->expectExceptionMessage(
+ Message::MANIFEST_INSPECTION_FAILED->getMessage($path),
+ );
+
+ (new ManifestLoader())->load($path);
+ }
+
public function testThrowManifestReadExceptionWhenManifestIsUnreadable(): void
{
$path = $this->temporaryManifest('{}');
@@ -225,6 +279,25 @@ public function testThrowManifestReadExceptionWhenManifestIsUnreadable(): void
}
}
+ public function testThrowManifestReadExceptionWhenReadingFails(): void
+ {
+ $path = $this->temporaryManifest('{}');
+
+ MockerState::addCondition(
+ 'PHPForge\\Vite\\Manifest',
+ 'file_get_contents',
+ [$path, false, null, 0, null],
+ false,
+ );
+
+ $this->expectException(ManifestReadException::class);
+ $this->expectExceptionMessage(
+ Message::MANIFEST_READ_FAILED->getMessage($path),
+ );
+
+ (new ManifestLoader())->load($path);
+ }
+
protected function tearDown(): void
{
foreach ($this->temporaryFiles as $path) {
diff --git a/tests/Provider/ConfigurationProvider.php b/tests/Provider/ConfigurationProvider.php
index 66ac44a..c25dae1 100644
--- a/tests/Provider/ConfigurationProvider.php
+++ b/tests/Provider/ConfigurationProvider.php
@@ -70,6 +70,10 @@ public static function invalidEntrypoints(): iterable
*/
public static function invalidProductionBaseUrls(): iterable
{
+ yield 'malformed absolute URL' => [
+ 'http://:',
+ Message::ASSET_BASE_URL_INVALID,
+ ];
yield 'unsafe scheme' => [
'javascript:alert(1)',
Message::ASSET_BASE_URL_SCHEME_INVALID,
@@ -101,6 +105,10 @@ public static function invalidProductionBaseUrls(): iterable
*/
public static function unsafeAssetUrls(): iterable
{
+ yield 'malformed absolute URL' => [
+ 'http://:',
+ Message::ASSET_URL_FORM_INVALID,
+ ];
yield 'empty' => [
'',
Message::ASSET_URL_FORM_UNSAFE,
diff --git a/tests/Provider/HtmlRendererProvider.php b/tests/Provider/HtmlRendererProvider.php
index db0bcf2..763cc96 100644
--- a/tests/Provider/HtmlRendererProvider.php
+++ b/tests/Provider/HtmlRendererProvider.php
@@ -13,7 +13,7 @@
final class HtmlRendererProvider
{
/**
- * @return iterable, Message, list}>
+ * @return iterable, Message, list}>
*/
public static function unsafeAttributes(): iterable
{
diff --git a/tests/Support/InternalMockerExtension.php b/tests/Support/InternalMockerExtension.php
new file mode 100644
index 0000000..e6763dc
--- /dev/null
+++ b/tests/Support/InternalMockerExtension.php
@@ -0,0 +1,63 @@
+registerSubscribers(
+ new class implements StartedSubscriber {
+ public function notify(Started $event): void
+ {
+ InternalMockerExtension::load();
+ }
+ },
+ new class implements PreparationStartedSubscriber {
+ public function notify(PreparationStarted $event): void
+ {
+ MockerState::resetState();
+ }
+ },
+ new class implements FinishedSubscriber {
+ public function notify(Finished $event): void
+ {
+ MockerState::resetState();
+ }
+ },
+ );
+ }
+
+ public static function load(): void
+ {
+ $mocker = new Mocker(
+ __DIR__ . '/../../runtime/.phpunit.cache/internal-mocker/mocks.php',
+ __DIR__ . '/internal-mocker-stubs.php',
+ );
+ $mocker->load(
+ [
+ [
+ 'namespace' => 'PHPForge\\Vite\\Manifest',
+ 'name' => 'file_get_contents',
+ ],
+ [
+ 'namespace' => 'PHPForge\\Vite\\Manifest',
+ 'name' => 'stat',
+ ],
+ ],
+ );
+
+ MockerState::saveState();
+ }
+}
diff --git a/tests/Support/internal-mocker-stubs.php b/tests/Support/internal-mocker-stubs.php
new file mode 100644
index 0000000..7d67058
--- /dev/null
+++ b/tests/Support/internal-mocker-stubs.php
@@ -0,0 +1,19 @@
+ 'string $filename, bool $use_include_path = false, $context = null, int $offset = 0, int|null $length = null',
+ 'arguments' => '$filename, $use_include_path, $context, $offset, $length',
+];
+
+return $stubs;
diff --git a/tests/ViteProductionTest.php b/tests/ViteProductionTest.php
index fd3a0cb..b08a84f 100644
--- a/tests/ViteProductionTest.php
+++ b/tests/ViteProductionTest.php
@@ -7,6 +7,7 @@
use PHPForge\Vite\Asset\{AssetCollection, ModulePreload, ModuleScript, Stylesheet};
use PHPForge\Vite\Configuration\ProductionConfiguration;
use PHPForge\Vite\Exception\{EntrypointNotFoundException, InvalidManifestException, Message};
+use PHPForge\Vite\Manifest\ManifestLoader;
use PHPForge\Vite\Vite;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
@@ -48,6 +49,26 @@ public function testCircularImportDoesNotPreloadRootEntrypoint(): void
);
}
+ public function testClearManifestCacheDiscardsTheProductionManifest(): void
+ {
+ $manifestPath = __DIR__ . '/Fixture/manifest.json';
+ $loader = new ManifestLoader();
+ $cachedManifest = $loader->load($manifestPath);
+ $vite = new Vite(
+ new ProductionConfiguration($manifestPath, '/build'),
+ ['views/foo.js'],
+ $loader,
+ );
+
+ $vite->clearManifestCache();
+
+ self::assertNotSame(
+ $cachedManifest,
+ $loader->load($manifestPath),
+ 'Clearing through the facade must discard the production manifest.',
+ );
+ }
+
public function testCssEntrypointProducesOnlyStylesheet(): void
{
$assets = $this->vite('css-entrypoint-manifest.json', ['resources/css/app.css'])->resolve();
@@ -94,9 +115,12 @@ public function testEntrypointsWithSameOutputFileAreDeduplicated(): void
)->resolve();
self::assertSame(
- [ModuleScript::class . ':/build/assets/bundle.js'],
+ [
+ ModuleScript::class . ':/build/assets/bundle.js',
+ ModulePreload::class . ':/build/assets/vendor.js',
+ ],
$this->describe($assets),
- 'A shared output file must appear once.',
+ 'A shared output file must appear once without suppressing later preloads.',
);
}