Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Vite asset url helper #43702

Merged
merged 20 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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