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()