Skip to content

Commit

Permalink
[9.x] Vite asset url helper (laravel#43702)
Browse files Browse the repository at this point in the history
* extract manifest file caching and access

* extract common functionality

* add Vite asset helper

* bring tests closer to reality

* add missing docblock

* fix visibility

* extract HMR check

* extract hot functions

* refactor hot asset to support passing an asset

* add support for hot assets

* cs

* use chunk

* fix

* ref build directly

* refactor

* remove preemptive support

* standardise naming

* exception tests

* restore unrelated function removal

* formatting

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
timacdonald and taylorotwell authored Aug 17, 2022
1 parent 7dc8cc0 commit ffee0c0
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 44 deletions.
126 changes: 97 additions & 29 deletions src/Illuminate/Foundation/Vite.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ class Vite
*/
protected $styleTagAttributesResolvers = [];

/**
* The cached manifest files.
*
* @var array
*/
protected static $manifests = [];

/**
* Get the Content Security Policy nonce applied to all generated tags.
*
Expand Down Expand Up @@ -116,49 +123,33 @@ public function useStyleTagAttributes($attributes)
*/
public function __invoke($entrypoints, $buildDirectory = 'build')
{
static $manifests = [];

$entrypoints = collect($entrypoints);
$buildDirectory = Str::start($buildDirectory, '/');

if (is_file(public_path('/hot'))) {
$url = rtrim(file_get_contents(public_path('/hot')));

if ($this->isRunningHot()) {
return new HtmlString(
$entrypoints
->prepend('@vite/client')
->map(fn ($entrypoint) => $this->makeTagForChunk($entrypoint, "{$url}/{$entrypoint}", null, null))
->map(fn ($entrypoint) => $this->makeTagForChunk($entrypoint, $this->hotAsset($entrypoint), null, null))
->join('')
);
}

$manifestPath = public_path($buildDirectory.'/manifest.json');

if (! isset($manifests[$manifestPath])) {
if (! is_file($manifestPath)) {
throw new Exception("Vite manifest not found at: {$manifestPath}");
}

$manifests[$manifestPath] = json_decode(file_get_contents($manifestPath), true);
}

$manifest = $manifests[$manifestPath];
$manifest = $this->manifest($buildDirectory);

$tags = collect();

foreach ($entrypoints as $entrypoint) {
if (! isset($manifest[$entrypoint])) {
throw new Exception("Unable to locate file in Vite manifest: {$entrypoint}.");
}
$chunk = $this->chunk($manifest, $entrypoint);

$tags->push($this->makeTagForChunk(
$entrypoint,
asset("{$buildDirectory}/{$manifest[$entrypoint]['file']}"),
$manifest[$entrypoint],
asset("{$buildDirectory}/{$chunk['file']}"),
$chunk,
$manifest
));

foreach ($manifest[$entrypoint]['css'] ?? [] as $css) {
foreach ($chunk['css'] ?? [] as $css) {
$partialManifest = Collection::make($manifest)->where('file', $css);

$tags->push($this->makeTagForChunk(
Expand All @@ -169,7 +160,7 @@ public function __invoke($entrypoints, $buildDirectory = 'build')
));
}

foreach ($manifest[$entrypoint]['imports'] ?? [] as $import) {
foreach ($chunk['imports'] ?? [] as $import) {
foreach ($manifest[$import]['css'] ?? [] as $css) {
$partialManifest = Collection::make($manifest)->where('file', $css);

Expand Down Expand Up @@ -378,25 +369,102 @@ protected function parseAttributes($attributes)
*/
public function reactRefresh()
{
if (! is_file(public_path('/hot'))) {
if (! $this->isRunningHot()) {
return;
}

$url = rtrim(file_get_contents(public_path('/hot')));

return new HtmlString(
sprintf(
<<<'HTML'
<script type="module">
import RefreshRuntime from '%s/@react-refresh'
import RefreshRuntime from '%s'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
HTML,
$url
$this->hotAsset('@react-refresh')
)
);
}

/**
* Get the path to a given asset when running in HMR mode.
*
* @return string
*/
protected function hotAsset($asset)
{
return rtrim(file_get_contents(public_path('/hot'))).'/'.$asset;
}

/**
* Get the URL for an asset.
*
* @param string $asset
* @param string|null $buildDirectory
* @return string
*/
public function asset($asset, $buildDirectory = 'build')
{
if ($this->isRunningHot()) {
return $this->hotAsset($asset);
}

$chunk = $this->chunk($this->manifest($buildDirectory), $asset);

return asset($buildDirectory.'/'.$chunk['file']);
}

/**
* Get the the manifest file for the given build directory.
*
* @param string $buildDirectory
* @return array
*
* @throws \Exception
*/
protected function manifest($buildDirectory)
{
$path = public_path($buildDirectory.'/manifest.json');

if (! isset(static::$manifests[$path])) {
if (! is_file($path)) {
throw new Exception("Vite manifest not found at: {$path}");
}

static::$manifests[$path] = json_decode(file_get_contents($path), true);
}

return static::$manifests[$path];
}

/**
* Get the chunk for the given entry point / asset.
*
* @param array $manifest
* @param string $file
* @return array
*
* @throws \Exception
*/
protected function chunk($manifest, $file)
{
if (! isset($manifest[$file])) {
throw new Exception("Unable to locate file in Vite manifest: {$file}.");
}

return $manifest[$file];
}

/**
* Determine if the HMR server is running.
*
* @return bool
*/
protected function isRunningHot()
{
return is_file(public_path('/hot'));
}
}
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/Vite.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/**
* @method static string useCspNonce(?string $nonce = null)
* @method static string|null cspNonce()
* @method static string asset(string $asset, string|null $buildDirectory)
* @method static \Illuminte\Foundation\Vite useIntegrityKey(string|false $key)
* @method static \Illuminte\Foundation\Vite useScriptTagAttributes(callable|array $callback)
* @method static \Illuminte\Foundation\Vite useStyleTagAttributes(callable|array $callback)
Expand Down
56 changes: 41 additions & 15 deletions tests/Foundation/FoundationViteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,25 @@

namespace Illuminate\Tests\Foundation;

use Exception;
use Illuminate\Foundation\Vite;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\Vite as ViteFacade;
use Illuminate\Support\Str;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Orchestra\Testbench\TestCase;

class FoundationViteTest extends TestCase
{
protected function setUp(): void
{
app()->instance('url', tap(
m::mock(UrlGenerator::class),
fn ($url) => $url
->shouldReceive('asset')
->andReturnUsing(fn ($value) => "https://example.com{$value}")
));

app()->singleton(Vite::class);
Facade::setFacadeApplication(app());
parent::setUp();

app('config')->set('app.asset_url', 'https://example.com');
}

protected function tearDown(): void
{
$this->cleanViteManifest();
$this->cleanViteHotFile();
Facade::clearResolvedInstances();
m::close();
}

public function testViteWithJsOnly()
Expand Down Expand Up @@ -513,6 +503,42 @@ public function testItCanOverrideAllAttributes()
);
}

public function testItCanGenerateIndividualAssetUrlInBuildMode()
{
$this->makeViteManifest();

$url = ViteFacade::asset('resources/js/app.js');

$this->assertSame('https://example.com/build/assets/app.versioned.js', $url);
}

public function testItCanGenerateIndividualAssetUrlInHotMode()
{
$this->makeViteHotFile();

$url = ViteFacade::asset('resources/js/app.js');

$this->assertSame('http://localhost:3000/resources/js/app.js', $url);
}

public function testItThrowsWhenUnableToFindAssetManifestInBuildMode()
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('Vite manifest not found at: '.public_path('build/manifest.json'));

ViteFacade::asset('resources/js/app.js');
}

public function testItThrowsWhenUnableToFindAssetChunkInBuildMode()
{
$this->makeViteManifest();

$this->expectException(Exception::class);
$this->expectExceptionMessage('Unable to locate file in Vite manifest: resources/js/missing.js');

ViteFacade::asset('resources/js/missing.js');
}

protected function makeViteManifest($contents = null, $path = 'build')
{
app()->singleton('path.public', fn () => __DIR__);
Expand Down

0 comments on commit ffee0c0

Please sign in to comment.