framework
framework copied to clipboard
[9.x] Add configurable paths to Vite
This PR is a followup of #43546.
As discussed, the following new functions should be added to the Vite class:
- New func
withEntryPointsthat accepts entry points and is merged into an array on the class. - New func
useHotFilethat accepts a path. - New func
useBuildDirectorythat accepts a path. - Make the
ViteclassHtmlable, with toHtml invoking the class.
- [x] Add new functions
- [x] Add tests
@timacdonald @jessarcher I think it's ready for review. Thanks!
Looking good! Just gonna mark as draft until we have the vite plugin PR ready. Gonna work on that and test everything out locally now. Thanks again for your work on this one.
The PR is dependent on: https://github.com/laravel/vite-plugin/pull/118
@iamgergo github isn't allow me to push changes to your branch for some reason, so I just applied some formatting adjustments via the UI.
Could you finally update the test with the following file. It brings the whole test filecloser to reality and allows us to remove some code we didn't actually need.
Updated test file
<?php
namespace Illuminate\Tests\Foundation;
use Illuminate\Foundation\Vite;
use Illuminate\Support\Facades\Vite as ViteFacade;
use Illuminate\Support\Str;
use Orchestra\Testbench\TestCase as TestbenchTestCase;
class FoundationViteTest extends TestbenchTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->app['config']->set('app.asset_url', 'https://example.com');
}
protected function tearDown(): void
{
$this->cleanViteManifest();
$this->cleanViteHotFile();
}
public function testViteWithJsOnly()
{
$this->makeViteManifest();
$result = app(Vite::class)('resources/js/app.js');
$this->assertSame('<script type="module" src="https://example.com/build/assets/app.versioned.js"></script>', $result->toHtml());
}
public function testViteWithCssAndJs()
{
$this->makeViteManifest();
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/app.versioned.css" />'
.'<script type="module" src="https://example.com/build/assets/app.versioned.js"></script>',
$result->toHtml()
);
}
public function testViteWithCssImport()
{
$this->makeViteManifest();
$result = app(Vite::class)('resources/js/app-with-css-import.js');
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/imported-css.versioned.css" />'
.'<script type="module" src="https://example.com/build/assets/app-with-css-import.versioned.js"></script>',
$result->toHtml()
);
}
public function testViteWithSharedCssImport()
{
$this->makeViteManifest();
$result = app(Vite::class)(['resources/js/app-with-shared-css.js']);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/shared-css.versioned.css" />'
.'<script type="module" src="https://example.com/build/assets/app-with-shared-css.versioned.js"></script>',
$result->toHtml()
);
}
public function testViteHotModuleReplacementWithJsOnly()
{
$this->makeViteHotFile();
$result = app(Vite::class)('resources/js/app.js');
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client"></script>'
.'<script type="module" src="http://localhost:3000/resources/js/app.js"></script>',
$result->toHtml()
);
}
public function testViteHotModuleReplacementWithJsAndCss()
{
$this->makeViteHotFile();
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client"></script>'
.'<link rel="stylesheet" href="http://localhost:3000/resources/css/app.css" />'
.'<script type="module" src="http://localhost:3000/resources/js/app.js"></script>',
$result->toHtml()
);
}
public function testItCanGenerateCspNonceWithHotFile()
{
Str::createRandomStringsUsing(fn ($length) => "random-string-with-length:{$length}");
$this->makeViteHotFile();
$nonce = ViteFacade::useCspNonce();
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame('random-string-with-length:40', $nonce);
$this->assertSame('random-string-with-length:40', ViteFacade::cspNonce());
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client" nonce="random-string-with-length:40"></script>'
.'<link rel="stylesheet" href="http://localhost:3000/resources/css/app.css" nonce="random-string-with-length:40" />'
.'<script type="module" src="http://localhost:3000/resources/js/app.js" nonce="random-string-with-length:40"></script>',
$result->toHtml()
);
Str::createRandomStringsNormally();
}
public function testItCanGenerateCspNonceWithManifest()
{
Str::createRandomStringsUsing(fn ($length) => "random-string-with-length:{$length}");
$this->makeViteManifest();
$nonce = ViteFacade::useCspNonce();
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame('random-string-with-length:40', $nonce);
$this->assertSame('random-string-with-length:40', ViteFacade::cspNonce());
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/app.versioned.css" nonce="random-string-with-length:40" />'
.'<script type="module" src="https://example.com/build/assets/app.versioned.js" nonce="random-string-with-length:40"></script>',
$result->toHtml()
);
Str::createRandomStringsNormally();
}
public function testItCanSpecifyCspNonceWithHotFile()
{
$this->makeViteHotFile();
$nonce = ViteFacade::useCspNonce('expected-nonce');
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame('expected-nonce', $nonce);
$this->assertSame('expected-nonce', ViteFacade::cspNonce());
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client" nonce="expected-nonce"></script>'
.'<link rel="stylesheet" href="http://localhost:3000/resources/css/app.css" nonce="expected-nonce" />'
.'<script type="module" src="http://localhost:3000/resources/js/app.js" nonce="expected-nonce"></script>',
$result->toHtml()
);
}
public function testItCanSpecifyCspNonceWithManifest()
{
$this->makeViteManifest();
$nonce = ViteFacade::useCspNonce('expected-nonce');
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame('expected-nonce', $nonce);
$this->assertSame('expected-nonce', ViteFacade::cspNonce());
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/app.versioned.css" nonce="expected-nonce" />'
.'<script type="module" src="https://example.com/build/assets/app.versioned.js" nonce="expected-nonce"></script>',
$result->toHtml()
);
}
public function testItCanInjectIntegrityWhenPresentInManifest()
{
$buildDir = Str::random();
$this->makeViteManifest([
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
'integrity' => 'expected-app.js-integrity',
],
'resources/css/app.css' => [
'file' => 'assets/app.versioned.css',
'integrity' => 'expected-app.css-integrity',
],
], $buildDir);
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js'], $buildDir);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/'.$buildDir.'/assets/app.versioned.css" integrity="expected-app.css-integrity" />'
.'<script type="module" src="https://example.com/'.$buildDir.'/assets/app.versioned.js" integrity="expected-app.js-integrity"></script>',
$result->toHtml()
);
unlink(public_path("{$buildDir}/manifest.json"));
rmdir(public_path($buildDir));
}
public function testItCanInjectIntegrityWhenPresentInManifestForImportedCss()
{
$buildDir = Str::random();
$this->makeViteManifest([
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
'imports' => [
'_import.versioned.js',
],
'integrity' => 'expected-app.js-integrity',
],
'_import.versioned.js' => [
'file' => 'assets/import.versioned.js',
'css' => [
'assets/imported-css.versioned.css',
],
'integrity' => 'expected-import.js-integrity',
],
'imported-css.css' => [
'file' => 'assets/imported-css.versioned.css',
'integrity' => 'expected-imported-css.css-integrity',
],
], $buildDir);
$result = app(Vite::class)('resources/js/app.js', $buildDir);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/'.$buildDir.'/assets/imported-css.versioned.css" integrity="expected-imported-css.css-integrity" />'
.'<script type="module" src="https://example.com/'.$buildDir.'/assets/app.versioned.js" integrity="expected-app.js-integrity"></script>',
$result->toHtml()
);
unlink(public_path("{$buildDir}/manifest.json"));
rmdir(public_path($buildDir));
}
public function testItCanSpecifyIntegrityKey()
{
$buildDir = Str::random();
$this->makeViteManifest([
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
'different-integrity-key' => 'expected-app.js-integrity',
],
'resources/css/app.css' => [
'file' => 'assets/app.versioned.css',
'different-integrity-key' => 'expected-app.css-integrity',
],
], $buildDir);
ViteFacade::useIntegrityKey('different-integrity-key');
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js'], $buildDir);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/'.$buildDir.'/assets/app.versioned.css" integrity="expected-app.css-integrity" />'
.'<script type="module" src="https://example.com/'.$buildDir.'/assets/app.versioned.js" integrity="expected-app.js-integrity"></script>',
$result->toHtml()
);
unlink(public_path("{$buildDir}/manifest.json"));
rmdir(public_path($buildDir));
}
public function testItCanSpecifyArbitraryAttributesForScriptTagsWhenBuilt()
{
$this->makeViteManifest();
ViteFacade::useScriptTagAttributes([
'general' => 'attribute',
]);
ViteFacade::useScriptTagAttributes(function ($src, $url, $chunk, $manifest) {
$this->assertSame('resources/js/app.js', $src);
$this->assertSame('https://example.com/build/assets/app.versioned.js', $url);
$this->assertSame(['file' => 'assets/app.versioned.js'], $chunk);
$this->assertSame([
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
],
'resources/js/app-with-css-import.js' => [
'file' => 'assets/app-with-css-import.versioned.js',
'css' => [
'assets/imported-css.versioned.css',
],
],
'resources/css/imported-css.css' => [
'file' => 'assets/imported-css.versioned.css',
],
'resources/js/app-with-shared-css.js' => [
'file' => 'assets/app-with-shared-css.versioned.js',
'imports' => [
'_someFile.js',
],
],
'resources/css/app.css' => [
'file' => 'assets/app.versioned.css',
],
'_someFile.js' => [
'css' => [
'assets/shared-css.versioned.css',
],
],
'resources/css/shared-css' => [
'file' => 'assets/shared-css.versioned.css',
],
], $manifest);
return [
'crossorigin',
'data-persistent-across-pages' => 'YES',
'remove-me' => false,
'keep-me' => true,
'null' => null,
'empty-string' => '',
'zero' => 0,
];
});
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/app.versioned.css" />'
.'<script type="module" src="https://example.com/build/assets/app.versioned.js" general="attribute" crossorigin data-persistent-across-pages="YES" keep-me empty-string="" zero="0"></script>',
$result->toHtml()
);
}
public function testItCanSpecifyArbitraryAttributesForStylesheetTagsWhenBuild()
{
$this->makeViteManifest();
ViteFacade::useStyleTagAttributes([
'general' => 'attribute',
]);
ViteFacade::useStyleTagAttributes(function ($src, $url, $chunk, $manifest) {
$this->assertSame('resources/css/app.css', $src);
$this->assertSame('https://example.com/build/assets/app.versioned.css', $url);
$this->assertSame(['file' => 'assets/app.versioned.css'], $chunk);
$this->assertSame([
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
],
'resources/js/app-with-css-import.js' => [
'file' => 'assets/app-with-css-import.versioned.js',
'css' => [
'assets/imported-css.versioned.css',
],
],
'resources/css/imported-css.css' => [
'file' => 'assets/imported-css.versioned.css',
],
'resources/js/app-with-shared-css.js' => [
'file' => 'assets/app-with-shared-css.versioned.js',
'imports' => [
'_someFile.js',
],
],
'resources/css/app.css' => [
'file' => 'assets/app.versioned.css',
],
'_someFile.js' => [
'css' => [
'assets/shared-css.versioned.css',
],
],
'resources/css/shared-css' => [
'file' => 'assets/shared-css.versioned.css',
],
], $manifest);
return [
'crossorigin',
'data-persistent-across-pages' => 'YES',
'remove-me' => false,
'keep-me' => true,
];
});
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<link rel="stylesheet" href="https://example.com/build/assets/app.versioned.css" general="attribute" crossorigin data-persistent-across-pages="YES" keep-me />'
.'<script type="module" src="https://example.com/build/assets/app.versioned.js"></script>',
$result->toHtml()
);
}
public function testItCanSpecifyArbitraryAttributesForScriptTagsWhenHotModuleReloading()
{
$this->makeViteHotFile();
ViteFacade::useScriptTagAttributes([
'general' => 'attribute',
]);
$expectedArguments = [
['src' => '@vite/client', 'url' => 'http://localhost:3000/@vite/client'],
['src' => 'resources/js/app.js', 'url' => 'http://localhost:3000/resources/js/app.js'],
];
ViteFacade::useScriptTagAttributes(function ($src, $url, $chunk, $manifest) use (&$expectedArguments) {
$args = array_shift($expectedArguments);
$this->assertSame($args['src'], $src);
$this->assertSame($args['url'], $url);
$this->assertNull($chunk);
$this->assertNull($manifest);
return [
'crossorigin',
'data-persistent-across-pages' => 'YES',
'remove-me' => false,
'keep-me' => true,
];
});
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client" general="attribute" crossorigin data-persistent-across-pages="YES" keep-me></script>'
.'<link rel="stylesheet" href="http://localhost:3000/resources/css/app.css" />'
.'<script type="module" src="http://localhost:3000/resources/js/app.js" general="attribute" crossorigin data-persistent-across-pages="YES" keep-me></script>',
$result->toHtml()
);
}
public function testItCanSpecifyArbitraryAttributesForStylesheetTagsWhenHotModuleReloading()
{
$this->makeViteHotFile();
ViteFacade::useStyleTagAttributes([
'general' => 'attribute',
]);
ViteFacade::useStyleTagAttributes(function ($src, $url, $chunk, $manifest) {
$this->assertSame('resources/css/app.css', $src);
$this->assertSame('http://localhost:3000/resources/css/app.css', $url);
$this->assertNull($chunk);
$this->assertNull($manifest);
return [
'crossorigin',
'data-persistent-across-pages' => 'YES',
'remove-me' => false,
'keep-me' => true,
];
});
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client"></script>'
.'<link rel="stylesheet" href="http://localhost:3000/resources/css/app.css" general="attribute" crossorigin data-persistent-across-pages="YES" keep-me />'
.'<script type="module" src="http://localhost:3000/resources/js/app.js"></script>',
$result->toHtml()
);
}
public function testItCanOverrideAllAttributes()
{
$this->makeViteManifest();
ViteFacade::useStyleTagAttributes([
'rel' => 'expected-rel',
'href' => 'expected-href',
]);
ViteFacade::useScriptTagAttributes([
'type' => 'expected-type',
'src' => 'expected-src',
]);
$result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
'<link rel="expected-rel" href="expected-href" />'
.'<script type="expected-type" src="expected-src"></script>',
$result->toHtml()
);
}
public function testViteCanMergeEntryPoints()
{
$this->makeViteManifest();
$vite = app(Vite::class);
$this->assertSame('', $vite->toHtml());
$vite->withEntryPoints(['resources/js/app.js']);
$this->assertSame(
'<script type="module" src="https://example.com/build/assets/app.versioned.js"></script>',
$vite->toHtml()
);
}
public function testViteCanOverrideBuildDirectory()
{
$this->makeViteManifest(null, 'custom-build');
$vite = app(Vite::class);
$vite->withEntryPoints(['resources/js/app.js'])->useBuildDirectory('custom-build');
$this->assertSame(
'<script type="module" src="https://example.com/custom-build/assets/app.versioned.js"></script>',
$vite->toHtml()
);
$this->cleanViteManifest('custom-build');
}
public function testViteCanOverrideHotFilePath()
{
$this->makeViteManifest();
$this->makeViteHotFile('build/hot');
$vite = app(Vite::class);
$vite->withEntryPoints(['resources/js/app.js'])->useHotFile(public_path('build/hot'));
$this->assertSame(
'<script type="module" src="http://localhost:3000/@vite/client"></script>'
.'<script type="module" src="http://localhost:3000/resources/js/app.js"></script>',
$vite->toHtml()
);
$this->cleanViteHotFile('build/hot');
}
protected function makeViteManifest($contents = null, $path = 'build')
{
app()->singleton('path.public', fn () => __DIR__);
if (! file_exists(public_path($path))) {
mkdir(public_path($path));
}
$manifest = json_encode($contents ?? [
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
],
'resources/js/app-with-css-import.js' => [
'file' => 'assets/app-with-css-import.versioned.js',
'css' => [
'assets/imported-css.versioned.css',
],
],
'resources/css/imported-css.css' => [
'file' => 'assets/imported-css.versioned.css',
],
'resources/js/app-with-shared-css.js' => [
'file' => 'assets/app-with-shared-css.versioned.js',
'imports' => [
'_someFile.js',
],
],
'resources/css/app.css' => [
'file' => 'assets/app.versioned.css',
],
'_someFile.js' => [
'css' => [
'assets/shared-css.versioned.css',
],
],
'resources/css/shared-css' => [
'file' => 'assets/shared-css.versioned.css',
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(public_path("{$path}/manifest.json"), $manifest);
}
protected function cleanViteManifest($path = 'build')
{
if (file_exists(public_path("{$path}/manifest.json"))) {
unlink(public_path("{$path}/manifest.json"));
}
if (file_exists(public_path($path))) {
rmdir(public_path($path));
}
}
protected function makeViteHotFile($path = 'hot')
{
app()->singleton('path.public', fn () => __DIR__);
file_put_contents(public_path($path), 'http://localhost:3000');
}
protected function cleanViteHotFile($path = 'hot')
{
if (file_exists(public_path($path))) {
unlink(public_path($path));
}
}
}
github isn't allow me to push changes to your branch for some reason, so I just applied some formatting adjustments via the UI.
Hm..that's strange. I updated the test file, thanks!
One additional thing we now need to consider...
public function boot()
{
Vite::useBuildDirectory('foo');
}
@vite('app.js', 'bar')
The build directory will now be ignored, so we will need to handle that as I believe the value passed through will always need to be respected as it is unexpected for it to be ignored when actually specified (which is the current behaviour)
I think we will also need to check how many arguments are passed through to the __invoke function to determine what build directory to utilise:
$buildDirectory = func_get_args()[1] ?? $this->buildDirectory ?? 'build';
Demo: https://3v4l.org/J6XeM
And what if we just simply do this:
public function __invoke($entryPoints, $buildDirectory = null)
{
$buildDirectory ??= $this->buildDirectory ?? 'build';
// ...
}
Or do you think it is a breaking change?
Love it and because we don't have types on the function I don't believe this is a breaking change. We will need to update the docblock to allow for string|null, but I do prefer your suggestion.
I'll try and take some time tomorrow (Tuesday my time) to review this thoroughly with the team and see if we can get it merged for Tuesday's release.
Awesome, I've updated the __invoke and toHtml methods then.
Thank you!
@iamgergo would it be possible to have you add me to your fork of the framework so I can push some final tweaks? I've also resolved the conflicts for us.
@timacdonald Okay, I sent you an invitation.
Thank you!
Just gave this one a final test against the plugin PR and all is working as expected. Thanks so much for your work on this @iamgergo and thanks for your patience with the feedback.
Documentation PR: https://github.com/laravel/docs/pull/8146
@timacdonald Great, thank you! I'm happy I could help a little!