diff --git a/src/Configuration.php b/src/Configuration.php
index 06b9aac..1cfb32f 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -32,7 +32,7 @@ public function __construct(
$this->heartbeatChecker ??= app(HeartbeatChecker::class);
$this->tagGenerator ??= app(TagGenerator::class);
}
-
+
/**
* Returns the manifest, reading it from the disk if necessary.
*
@@ -62,6 +62,10 @@ public function getManifestPath(): string
}
}
+ if (str_starts_with($this->config('build_path'), 'http')) {
+ return sprintf('%s/%s', trim($this->config('build_path'), '/\\'), 'manifest.json');
+ }
+
return str_replace(
['\\', '//'],
'/',
@@ -74,7 +78,13 @@ public function getManifestPath(): string
*/
public function getHash(): string|null
{
- if (!file_exists($path = $this->getManifestPath())) {
+ $path = $this->getManifestPath();
+
+ if (str_starts_with($path, 'http')) {
+ return md5(Manifest::getManifestContent($path));
+ }
+
+ if (!file_exists($path)) {
return null;
}
@@ -122,7 +132,7 @@ public function getTags(): string
->map(fn ($entrypoint) => (string) $entrypoint)
->join('');
}
-
+
/**
* Gets the script tag for the client module.
*/
@@ -261,7 +271,7 @@ protected function shouldUseManifest(): bool
return $result;
}
}
-
+
// If the development server is disabled, use the manifest.
if (!$this->config('dev_server.enabled', true)) {
return true;
diff --git a/src/Manifest.php b/src/Manifest.php
index d5aa563..080abff 100644
--- a/src/Manifest.php
+++ b/src/Manifest.php
@@ -3,6 +3,7 @@
namespace Innocenzi\Vite;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Innocenzi\Vite\Exceptions\ManifestNotFoundException;
use Innocenzi\Vite\Exceptions\NoSuchEntrypointException;
@@ -21,12 +22,12 @@ final class Manifest implements Stringable
public function __construct(protected string|null $path)
{
$this->path = str_replace('\\', '/', $path);
-
- if (!$path || !file_exists($path)) {
+
+ if (!$manifest = static::getManifestContent($path)) {
throw new ManifestNotFoundException($path, static::guessConfigName($path));
}
- $this->chunks = Collection::make(json_decode(file_get_contents($path), true));
+ $this->chunks = Collection::make(json_decode($manifest, true, 512, \JSON_THROW_ON_ERROR));
$this->entries = $this->chunks
->map(fn (array $value) => Chunk::fromArray($this, $value))
->filter(fn (Chunk $entry) => $entry->isEntry);
@@ -83,7 +84,7 @@ public static function guessConfigName(string $path): string|null
{
$path = str_replace(['\\', '//'], '/', $path);
$public = str_replace(['\\', '//'], '/', public_path());
- $inferredBuildPath = (string) Str::of($path)->beforeLast('/manifest.json')->replace($public, '')->trim('/');
+ $inferredBuildPath = (string)Str::of($path)->beforeLast('/manifest.json')->replace($public, '')->trim('/');
[$name] = collect(config('vite.configs'))
->map(fn ($config, $name) => [$name, $config['build_path']])
@@ -92,6 +93,26 @@ public static function guessConfigName(string $path): string|null
return $name;
}
+ /**
+ * Fetches the manifest's contents from the given path.
+ */
+ public static function getManifestContent(string|null $path): string|null
+ {
+ if (str_starts_with($path, 'http')) {
+ return cache()->remember(
+ config('vite.remote_manifest.cache_key', 'vite.remote_manifest'),
+ config('vite.remote_manifest.cache_duration', now()->addHour()),
+ fn () => Http::get($path)->body()
+ );
+ }
+
+ if (file_exists($path)) {
+ return file_get_contents($path);
+ }
+
+ return null;
+ }
+
/**
* Gets entries as HTML.
*/
diff --git a/tests/Features/ConfigurationTest.php b/tests/Features/ConfigurationTest.php
index cecd5ba..a5b3cb0 100644
--- a/tests/Features/ConfigurationTest.php
+++ b/tests/Features/ConfigurationTest.php
@@ -1,11 +1,13 @@
Vite::$useManifestCallback = null);
@@ -27,7 +29,7 @@
it('generates URLs relative to the app URL by default in production', function () {
set_fixtures_path('builds');
set_env('production');
-
+
expect(using_manifest('builds/public/with-css/manifest.json')->getTags())
->toContain('')
->toContain('');
@@ -40,7 +42,7 @@
$property = new ReflectionProperty(UrlGenerator::class, 'assetRoot');
$property->setAccessible(true);
$property->setValue(app('url'), 'https://s3.us-west-2.amazonaws.com/12345678');
-
+
expect(using_manifest('builds/public/with-css/manifest.json')->getTags())
->toContain('')
->toContain('');
@@ -69,10 +71,10 @@
set_vite_config('default', ['build_path' => '/']);
expect(vite()->getManifestPath())->toBe(str_replace('\\', '/', public_path('manifest.json')));
-
+
set_vite_config('default', ['build_path' => '/build']);
expect(vite()->getManifestPath())->toBe(str_replace('\\', '/', public_path('build/manifest.json')));
-
+
set_vite_config('default', ['build_path' => '/build/']);
expect(vite()->getManifestPath())->toBe(str_replace('\\', '/', public_path('build/manifest.json')));
});
@@ -80,17 +82,36 @@
it('finds the manifest version', function () {
set_fixtures_path('builds');
set_env('production');
-
+
set_vite_config('default', ['build_path' => '']);
expect(vite()->getHash())->toBeNull();
-
+
set_vite_config('default', ['build_path' => 'with-css']);
expect(vite()->getHash())->toBe(md5_file(fixtures_path('builds/public/with-css/manifest.json')));
-
+
set_vite_config('default', ['build_path' => 'with-integrity']);
expect(vite()->getHash())->toBe(md5_file(fixtures_path('builds/public/with-integrity/manifest.json')));
});
+it('finds the remote manifest version', function () {
+ set_vite_config('default', [
+ 'build_path' => 'http://manifest.test/build',
+ ]);
+
+ $content = file_get_contents(
+ fixtures_path('manifests/with-entries.json')
+ );
+
+ $hash = md5($content);
+
+ Http::fake([
+ 'manifest.test/build/manifest.json' => Http::response($content),
+ ]);
+
+ $configuration = new Configuration('default');
+ assertEquals($hash, $configuration->getHash());
+});
+
it('finds a configured entrypoint by its name in development', function () {
with_dev_server();
set_fixtures_path('');
@@ -120,7 +141,7 @@
it('returns a valid asset URL in development', function () {
with_dev_server();
set_env('local');
-
+
set_vite_config('default', ['build_path' => '/should-not-be/included']);
expect(vite()->getAssetUrl('/my-custom-asset.txt'))->toContain('http://localhost:5173/my-custom-asset.txt');
expect(vite()->getAssetUrl('without-leading-slash.txt'))->toContain('http://localhost:5173/without-leading-slash.txt');
@@ -128,16 +149,16 @@
it('returns a valid asset URL in production', function () {
set_env('production');
-
+
set_vite_config('default', ['build_path' => '/with/slashes/']);
expect(vite()->getAssetUrl('/my-custom-asset.txt'))->toContain('http://localhost/with/slashes/my-custom-asset.txt');
-
+
set_vite_config('default', ['build_path' => '/with/leading/slash']);
expect(vite()->getAssetUrl('/my-custom-asset.txt'))->toContain('http://localhost/with/leading/slash/my-custom-asset.txt');
-
+
set_vite_config('default', ['build_path' => 'with/trailing/slash/']);
expect(vite()->getAssetUrl('/my-custom-asset.txt'))->toContain('http://localhost/with/trailing/slash/my-custom-asset.txt');
-
+
set_vite_config('default', ['build_path' => 'build']);
expect(vite()->getAssetUrl('/my-custom-asset.txt'))->toContain('http://localhost/build/my-custom-asset.txt');
expect(vite()->getAssetUrl('my-custom/asset.txt'))->toContain('http://localhost/build/my-custom/asset.txt');
@@ -145,14 +166,14 @@
$property = new ReflectionProperty(UrlGenerator::class, 'assetRoot');
$property->setAccessible(true);
$property->setValue(app('url'), 'https://s3.us-west-2.amazonaws.com/12345678');
-
+
expect(vite()->getAssetUrl('/my-custom-asset.txt'))
->toContain('https://s3.us-west-2.amazonaws.com/12345678/build/my-custom-asset');
});
it('respects the mode override in production', function () {
set_env('production');
-
+
expect(vite()->usesManifest())->toBeTrue();
Vite::useManifest(fn () => false);
@@ -163,7 +184,7 @@
it('respects the mode override in development', function () {
with_dev_server(reacheable: true);
set_env('local');
-
+
expect(vite()->usesManifest())->toBeFalse();
Vite::useManifest(fn () => true);
@@ -185,10 +206,10 @@
Vite::useManifest(fn () => false);
expect(vite()->usesManifest())->toBeFalse();
-
+
Vite::useManifest(fn () => null);
expect(vite()->usesManifest())->toBeTrue();
-
+
with_dev_server(reacheable: true);
set_env('local');
expect(vite()->usesManifest())->toBeFalse();
diff --git a/tests/Features/ManifestTest.php b/tests/Features/ManifestTest.php
index 7fb7a27..bd3557f 100644
--- a/tests/Features/ManifestTest.php
+++ b/tests/Features/ManifestTest.php
@@ -1,5 +1,6 @@
getTag('main'));
})->throws(NoSuchEntrypointException::class);
+it('can load manifest from remote', function () {
+ set_vite_config('default', [
+ 'build_path' => 'http://manifest.test/build',
+ ]);
+
+ Http::fake([
+ 'manifest.test/build/manifest.json' => Http::response(
+ file_get_contents(
+ fixtures_path('manifests/with-entries.json')
+ )
+ ),
+ ]);
+
+ $configuration = new Configuration('default');
+ $manifest = $configuration->getManifest();
+
+ expect($manifest->getEntries()
+ ->keys())
+ ->toContain('resources/scripts/main.ts')
+ ->toContain('resources/scripts/entry.ts')
+ ->toHaveCount(2);
+});
+
it('throws when trying to access an entry that does not exist', function () {
get_manifest('with-entries.json')->getEntry('this-entry-does-not-exist');
})->throws(NoSuchEntrypointException::class);