From ec5210245556666d96d7dd23133f1c905e603d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A3=E6=B8=A3120?= <52521836+WOSHIZHAZHA120@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:18:15 +0800 Subject: [PATCH] feat: support remote manifests (#309) Co-authored-by: Enzo Innocenzi --- src/Configuration.php | 18 +++++++-- src/Manifest.php | 29 +++++++++++++-- tests/Features/ConfigurationTest.php | 55 +++++++++++++++++++--------- tests/Features/ManifestTest.php | 24 ++++++++++++ 4 files changed, 101 insertions(+), 25 deletions(-) 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);