diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php
index 2deea7af44bf..4c17a83e9b42 100644
--- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php
+++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php
@@ -4,6 +4,7 @@
use Illuminate\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract;
use Illuminate\Foundation\MaintenanceModeManager;
+use Illuminate\Foundation\Vite;
use Illuminate\Http\Request;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\AggregateServiceProvider;
@@ -24,6 +25,15 @@ class FoundationServiceProvider extends AggregateServiceProvider
ParallelTestingServiceProvider::class,
];
+ /**
+ * The singletons to register into the container.
+ *
+ * @var array
+ */
+ public $singletons = [
+ Vite::class => Vite::class,
+ ];
+
/**
* Boot the service provider.
*
diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php
index 717958ee7840..67f2b298ddfd 100644
--- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php
+++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php
@@ -120,6 +120,21 @@ public function __call($name, $arguments)
{
return '';
}
+
+ public function useIntegrityKey()
+ {
+ return $this;
+ }
+
+ public function useScriptTagAttributes()
+ {
+ return $this;
+ }
+
+ public function useStyleTagAttributes()
+ {
+ return $this;
+ }
});
return $this;
diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php
index 7e641ab7b606..b8635d6b1220 100644
--- a/src/Illuminate/Foundation/Vite.php
+++ b/src/Illuminate/Foundation/Vite.php
@@ -3,11 +3,108 @@
namespace Illuminate\Foundation;
use Exception;
+use Illuminate\Support\Collection;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
class Vite
{
+ /**
+ * The Content Security Policy nonce to apply to all generated tags.
+ *
+ * @var string|null
+ */
+ protected $nonce;
+
+ /**
+ * The key to check for integrity hashes within the manifest.
+ *
+ * @var string|false
+ */
+ protected $integrityKey = 'integrity';
+
+ /**
+ * The script tag attributes resolvers.
+ *
+ * @var array
+ */
+ protected $scriptTagAttributesResolvers = [];
+
+ /**
+ * The style tag attributes resolvers.
+ *
+ * @var array
+ */
+ protected $styleTagAttributesResolvers = [];
+
+ /**
+ * Get the Content Security Policy nonce applied to all generated tags.
+ *
+ * @return string|null
+ */
+ public function cspNonce()
+ {
+ return $this->nonce;
+ }
+
+ /**
+ * Generate or set a Content Security Policy nonce to apply to all generated tags.
+ *
+ * @param ?string $nonce
+ * @return string
+ */
+ public function useCspNonce($nonce = null)
+ {
+ return $this->nonce = $nonce ?? Str::random(40);
+ }
+
+ /**
+ * Use the given key to detect integrity hashes in the manifest.
+ *
+ * @param string|false $key
+ * @return $this
+ */
+ public function useIntegrityKey($key)
+ {
+ $this->integrityKey = $key;
+
+ return $this;
+ }
+
+ /**
+ * Use the given callback to resolve attributes for script tags.
+ *
+ * @param (callable(string, string, ?array, ?array): array)|array $attributes
+ * @return $this
+ */
+ public function useScriptTagAttributes($attributes)
+ {
+ if (! is_callable($attributes)) {
+ $attributes = fn () => $attributes;
+ }
+
+ $this->scriptTagAttributesResolvers[] = $attributes;
+
+ return $this;
+ }
+
+ /**
+ * Use the given callback to resolve attributes for style tags.
+ *
+ * @param (callable(string, string, ?array, ?array): array)|array $attributes
+ * @return $this
+ */
+ public function useStyleTagAttributes($attributes)
+ {
+ if (! is_callable($attributes)) {
+ $attributes = fn () => $attributes;
+ }
+
+ $this->styleTagAttributesResolvers[] = $attributes;
+
+ return $this;
+ }
+
/**
* Generate Vite tags for an entrypoint.
*
@@ -29,8 +126,8 @@ public function __invoke($entrypoints, $buildDirectory = 'build')
return new HtmlString(
$entrypoints
- ->map(fn ($entrypoint) => $this->makeTag("{$url}/{$entrypoint}"))
- ->prepend($this->makeScriptTag("{$url}/@vite/client"))
+ ->prepend('@vite/client')
+ ->map(fn ($entrypoint) => $this->makeTagForChunk($entrypoint, "{$url}/{$entrypoint}", null, null))
->join('')
);
}
@@ -54,21 +151,32 @@ public function __invoke($entrypoints, $buildDirectory = 'build')
throw new Exception("Unable to locate file in Vite manifest: {$entrypoint}.");
}
- $tags->push($this->makeTag(asset("{$buildDirectory}/{$manifest[$entrypoint]['file']}")));
+ $tags->push($this->makeTagForChunk(
+ $entrypoint,
+ asset("{$buildDirectory}/{$manifest[$entrypoint]['file']}"),
+ $manifest[$entrypoint],
+ $manifest
+ ));
- if (isset($manifest[$entrypoint]['css'])) {
- foreach ($manifest[$entrypoint]['css'] as $css) {
- $tags->push($this->makeStylesheetTag(asset("{$buildDirectory}/{$css}")));
- }
+ foreach ($manifest[$entrypoint]['css'] ?? [] as $css) {
+ $tags->push($this->makeTagForChunk(
+ $entrypoint,
+ asset("{$buildDirectory}/{$css}"),
+ $manifest[$entrypoint],
+ $manifest
+ ));
}
- if (isset($manifest[$entrypoint]['imports'])) {
- foreach ($manifest[$entrypoint]['imports'] as $import) {
- if (isset($manifest[$import]['css'])) {
- foreach ($manifest[$import]['css'] as $css) {
- $tags->push($this->makeStylesheetTag(asset("{$buildDirectory}/{$css}")));
- }
- }
+ foreach ($manifest[$entrypoint]['imports'] ?? [] as $import) {
+ foreach ($manifest[$import]['css'] ?? [] as $css) {
+ $partialManifest = Collection::make($manifest)->where('file', $css);
+
+ $tags->push($this->makeTagForChunk(
+ $partialManifest->keys()->first(),
+ asset("{$buildDirectory}/{$css}"),
+ $partialManifest->first(),
+ $manifest
+ ));
}
}
}
@@ -79,36 +187,86 @@ public function __invoke($entrypoints, $buildDirectory = 'build')
}
/**
- * Generate React refresh runtime script.
+ * Make tag for the given chunk.
*
- * @return \Illuminate\Support\HtmlString|void
+ * @param string $src
+ * @param string $url
+ * @param ?array $chunk
+ * @param ?array $manifest
+ * @return string
*/
- public function reactRefresh()
+ protected function makeTagForChunk($src, $url, $chunk, $manifest)
{
- if (! is_file(public_path('/hot'))) {
- return;
+ if (
+ $this->nonce === null
+ && $this->integrityKey !== false
+ && ! array_key_exists($this->integrityKey, $chunk ?? [])
+ && $this->scriptTagAttributesResolvers === []
+ && $this->styleTagAttributesResolvers === []) {
+ return $this->makeTag($url);
}
- $url = rtrim(file_get_contents(public_path('/hot')));
+ if ($this->isCssPath($url)) {
+ return $this->makeStylesheetTagWithAttributes(
+ $url,
+ $this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
+ );
+ }
- return new HtmlString(
- sprintf(
- <<<'HTML'
-
- HTML,
- $url
- )
+ return $this->makeScriptTagWithAttributes(
+ $url,
+ $this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)
);
}
/**
- * Generate an appropriate tag for the given URL.
+ * Resolve the attributes for the chunks generated script tag.
+ *
+ * @param string $src
+ * @param string $url
+ * @param ?array $chunk
+ * @param ?array $manifest
+ * @return array
+ */
+ protected function resolveScriptTagAttributes($src, $url, $chunk, $manifest)
+ {
+ $attributes = $this->integrityKey !== false
+ ? ['integrity' => $chunk[$this->integrityKey] ?? false]
+ : [];
+
+ foreach ($this->scriptTagAttributesResolvers as $resolver) {
+ $attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Resolve the attributes for the chunks generated stylesheet tag.
+ *
+ * @param string $src
+ * @param string $url
+ * @param ?array $chunk
+ * @param ?array $manifest
+ * @return array
+ */
+ protected function resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
+ {
+ $attributes = $this->integrityKey !== false
+ ? ['integrity' => $chunk[$this->integrityKey] ?? false]
+ : [];
+
+ foreach ($this->styleTagAttributesResolvers as $resolver) {
+ $attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Generate an appropriate tag for the given URL in HMR mode.
+ *
+ * @deprecated Will be removed in a future Laravel version.
*
* @param string $url
* @return string
@@ -125,23 +283,63 @@ protected function makeTag($url)
/**
* Generate a script tag for the given URL.
*
+ * @deprecated Will be removed in a future Laravel version.
+ *
* @param string $url
* @return string
*/
protected function makeScriptTag($url)
{
- return sprintf('', $url);
+ return $this->makeScriptTagWithAttributes($url, []);
}
/**
- * Generate a stylesheet tag for the given URL.
+ * Generate a stylesheet tag for the given URL in HMR mode.
+ *
+ * @deprecated Will be removed in a future Laravel version.
*
* @param string $url
* @return string
*/
protected function makeStylesheetTag($url)
{
- return sprintf('', $url);
+ return $this->makeStylesheetTagWithAttributes($url, []);
+ }
+
+ /**
+ * Generate a script tag with attributes for the given URL.
+ *
+ * @param string $url
+ * @param array $attributes
+ * @return string
+ */
+ protected function makeScriptTagWithAttributes($url, $attributes)
+ {
+ $attributes = $this->parseAttributes(array_merge([
+ 'type' => 'module',
+ 'src' => $url,
+ 'nonce' => $this->nonce ?? false,
+ ], $attributes));
+
+ return '';
+ }
+
+ /**
+ * Generate a link tag with attributes for the given URL.
+ *
+ * @param string $url
+ * @param array $attributes
+ * @return string
+ */
+ protected function makeStylesheetTagWithAttributes($url, $attributes)
+ {
+ $attributes = $this->parseAttributes(array_merge([
+ 'rel' => 'stylesheet',
+ 'href' => $url,
+ 'nonce' => $this->nonce ?? false,
+ ], $attributes));
+
+ return '';
}
/**
@@ -154,4 +352,49 @@ protected function isCssPath($path)
{
return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1;
}
+
+ /**
+ * Parse the attributes into key="value" strings.
+ *
+ * @param array $attributes
+ * @return array
+ */
+ protected function parseAttributes($attributes)
+ {
+ return Collection::make($attributes)
+ ->reject(fn ($value, $key) => in_array($value, [false, null], true))
+ ->flatMap(fn ($value, $key) => $value === true ? [$key] : [$key => $value])
+ ->map(fn ($value, $key) => is_int($key) ? $value : $key.'="'.$value.'"')
+ ->values()
+ ->all();
+ }
+
+ /**
+ * Generate React refresh runtime script.
+ *
+ * @return \Illuminate\Support\HtmlString|void
+ */
+ public function reactRefresh()
+ {
+ if (! is_file(public_path('/hot'))) {
+ return;
+ }
+
+ $url = rtrim(file_get_contents(public_path('/hot')));
+
+ return new HtmlString(
+ sprintf(
+ <<<'HTML'
+
+ HTML,
+ $url
+ )
+ );
+ }
}
diff --git a/src/Illuminate/Support/Facades/Facade.php b/src/Illuminate/Support/Facades/Facade.php
index d17e16c7c044..219e599be2e9 100755
--- a/src/Illuminate/Support/Facades/Facade.php
+++ b/src/Illuminate/Support/Facades/Facade.php
@@ -293,6 +293,7 @@ public static function defaultAliases()
'URL' => URL::class,
'Validator' => Validator::class,
'View' => View::class,
+ 'Vite' => Vite::class,
]);
}
diff --git a/src/Illuminate/Support/Facades/Vite.php b/src/Illuminate/Support/Facades/Vite.php
new file mode 100644
index 000000000000..610dc823016d
--- /dev/null
+++ b/src/Illuminate/Support/Facades/Vite.php
@@ -0,0 +1,25 @@
+shouldReceive('asset')
->andReturnUsing(fn ($value) => "https://example.com{$value}")
));
+
+ app()->singleton(Vite::class);
+ Facade::setFacadeApplication(app());
}
protected function tearDown(): void
{
$this->cleanViteManifest();
$this->cleanViteHotFile();
+ Facade::clearResolvedInstances();
m::close();
}
@@ -30,7 +37,7 @@ public function testViteWithJsOnly()
{
$this->makeViteManifest();
- $result = (new Vite)('resources/js/app.js');
+ $result = app(Vite::class)('resources/js/app.js');
$this->assertSame('', $result->toHtml());
}
@@ -39,7 +46,7 @@ public function testViteWithCssAndJs()
{
$this->makeViteManifest();
- $result = (new Vite)(['resources/css/app.css', 'resources/js/app.js']);
+ $result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
''
@@ -52,7 +59,7 @@ public function testViteWithCssImport()
{
$this->makeViteManifest();
- $result = (new Vite)('resources/js/app-with-css-import.js');
+ $result = app(Vite::class)('resources/js/app-with-css-import.js');
$this->assertSame(
''
@@ -65,7 +72,7 @@ public function testViteWithSharedCssImport()
{
$this->makeViteManifest();
- $result = (new Vite)(['resources/js/app-with-shared-css.js']);
+ $result = app(Vite::class)(['resources/js/app-with-shared-css.js']);
$this->assertSame(
''
@@ -78,7 +85,7 @@ public function testViteHotModuleReplacementWithJsOnly()
{
$this->makeViteHotFile();
- $result = (new Vite)('resources/js/app.js');
+ $result = app(Vite::class)('resources/js/app.js');
$this->assertSame(
''
@@ -91,7 +98,7 @@ public function testViteHotModuleReplacementWithJsAndCss()
{
$this->makeViteHotFile();
- $result = (new Vite)(['resources/css/app.css', 'resources/js/app.js']);
+ $result = app(Vite::class)(['resources/css/app.css', 'resources/js/app.js']);
$this->assertSame(
''
@@ -101,15 +108,384 @@ public function testViteHotModuleReplacementWithJsAndCss()
);
}
- protected function makeViteManifest()
+ 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(
+ ''
+ .''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .'',
+ $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(
+ ''
+ .''
+ .'',
+ $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(
+ ''
+ .''
+ .'',
+ $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(
+ ''
+ .'',
+ $result->toHtml()
+ );
+ }
+
+ protected function makeViteManifest($contents = null, $path = 'build')
{
app()->singleton('path.public', fn () => __DIR__);
- if (! file_exists(public_path('build'))) {
- mkdir(public_path('build'));
+ if (! file_exists(public_path($path))) {
+ mkdir(public_path($path));
}
- $manifest = json_encode([
+ $manifest = json_encode($contents ?? [
'resources/js/app.js' => [
'file' => 'assets/app.versioned.js',
],
@@ -119,6 +495,9 @@ protected function makeViteManifest()
'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' => [
@@ -133,9 +512,12 @@ protected function makeViteManifest()
'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('build/manifest.json'), $manifest);
+ file_put_contents(public_path("{$path}/manifest.json"), $manifest);
}
protected function cleanViteManifest()