-
Notifications
You must be signed in to change notification settings - Fork 11.1k
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] Add configurable paths to Vite #43620
Conversation
@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: laravel/vite-plugin#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));
}
}
} |
Hm..that's strange. I updated the test file, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would love @jessarcher eye on this one as well as the associated plugin PR
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
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 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 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: laravel/docs#8146 |
@timacdonald Great, thank you! I'm happy I could help a little! |
* [9.x] Add configurable paths to Vite * Update facade * Add tests * Revert import * Do not enforce starting slashes * Add public path resolver * formatting * resolve build directory from invoke * formatting * formatting * move method Co-authored-by: Tim MacDonald <hello@timacdonald.me> Co-authored-by: Taylor Otwell <taylor@laravel.com>
This PR is a followup of #43546.
As discussed, the following new functions should be added to the
Vite
class:withEntryPoints
that accepts entry points and is merged into an array on the class.useHotFile
that accepts a path.useBuildDirectory
that accepts a path.Vite
classHtmlable
, with toHtml invoking the class.