Skip to content

Commit

Permalink
feat: support remote manifests (#309)
Browse files Browse the repository at this point in the history
Co-authored-by: Enzo Innocenzi <enzo@innocenzi.dev>
  • Loading branch information
WOSHIZHAZHA120 and innocenzi authored Sep 2, 2022
1 parent c73c8e7 commit ec52102
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 25 deletions.
18 changes: 14 additions & 4 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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(
['\\', '//'],
'/',
Expand All @@ -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;
}

Expand Down Expand Up @@ -122,7 +132,7 @@ public function getTags(): string
->map(fn ($entrypoint) => (string) $entrypoint)
->join('');
}

/**
* Gets the script tag for the client module.
*/
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 25 additions & 4 deletions src/Manifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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']])
Expand All @@ -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.
*/
Expand Down
55 changes: 38 additions & 17 deletions tests/Features/ConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?php

use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Http;
use Innocenzi\Vite\Configuration;
use Innocenzi\Vite\Exceptions\NoBuildPathException;
use Innocenzi\Vite\Exceptions\NoSuchConfigurationException;
use Innocenzi\Vite\Exceptions\NoSuchEntrypointException;
use Innocenzi\Vite\Vite;
use function PHPUnit\Framework\assertEquals;

afterAll(fn () => Vite::$useManifestCallback = null);

Expand All @@ -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('<link rel="stylesheet" href="http://localhost/with-css/assets/test.65bd481b.css" />')
->toContain('<script type="module" src="http://localhost/with-css/assets/test.a2c636dd.js"></script>');
Expand All @@ -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('<link rel="stylesheet" href="https://s3.us-west-2.amazonaws.com/12345678/with-css/assets/test.65bd481b.css" />')
->toContain('<script type="module" src="https://s3.us-west-2.amazonaws.com/12345678/with-css/assets/test.a2c636dd.js"></script>');
Expand Down Expand Up @@ -69,28 +71,47 @@

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')));
});

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('');
Expand Down Expand Up @@ -120,39 +141,39 @@
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');
});

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');

$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);
Expand All @@ -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);
Expand All @@ -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();
Expand Down
24 changes: 24 additions & 0 deletions tests/Features/ManifestTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Illuminate\Support\Facades\Http;
use Innocenzi\Vite\Configuration;
use Innocenzi\Vite\Exceptions\ManifestNotFoundException;
use Innocenzi\Vite\Exceptions\NoSuchEntrypointException;
Expand Down Expand Up @@ -91,6 +92,29 @@
expect(vite()->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);
Expand Down

0 comments on commit ec52102

Please # to comment.